001// Copyright 2005 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry.util;
016
017import java.util.ArrayList;
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.hivemind.ApplicationRuntimeException;
026import org.apache.hivemind.util.Defense;
027import org.apache.tapestry.components.IPrimaryKeyConverter;
028
029/**
030 * Companion to the {@link org.apache.tapestry.components.ForBean For component}, this class is an
031 * implementation of {@link org.apache.tapestry.components.IPrimaryKeyConverter} that performs some
032 * additional handling, such as tracking value sets..
033 * <p>
034 * Value sets are sets of value objects maintained by the converter; the converter will provide a
035 * synthetic read/write boolean property that indicates if the {@link #getLastValue() last value} is
036 * or is not in the set.
037 * <p>
038 * A single built-in value set, {@link #isDeleted()} has a special purpose; it controls whether or
039 * not values are returned from {@link #getValues()}. Subclasses may add additional synthetic
040 * boolean properties and additional sets.
041 * <p>
042 * Why not just store a boolean property in the object itself? Well, deleted is a good example of a
043 * property that is meaningful in the context of an operation, but isn't stored ... once an object
044 * is deleted (from secondary storage, such as a database) there's no place to store such a flag.
045 * The DefaultPrimaryKey converter is used in this context to store transient, operation data ...
046 * such as which values are to be deleted.
047 * <p>
048 * This class can be thought of as a successor to {@link org.apache.tapestry.form.ListEditMap}.
049 * 
050 * @author Howard M. Lewis Ship
051 * @since 4.0
052 */
053public class DefaultPrimaryKeyConverter implements IPrimaryKeyConverter
054{
055    private final Map _map = new HashMap();
056
057    private final List _keys = new ArrayList();
058
059    // The values added to the Map, in the order they were added.
060    private final List _values = new ArrayList();
061
062    // The last value accessed by getPrimaryKey() or getValue().
063    // Other methods may operate upon this value.
064
065    private Object _lastValue;
066
067    private Set _deletedValues;
068
069    /**
070     * Clears all properties of the converter, returning it to a pristine state. Subclasses should
071     * invoke this implementation in addition to clearing any of their own state.
072     */
073    public void clear()
074    {
075        _map.clear();
076        _keys.clear();
077        _values.clear();
078        _lastValue = null;
079        _deletedValues = null;
080    }
081
082    public final void add(Object key, Object value)
083    {
084        Defense.notNull(key, "key");
085        Defense.notNull(value, "value");
086
087        if (_map.containsKey(key))
088            throw new ApplicationRuntimeException(UtilMessages.keyAlreadyExists(key));
089
090        _map.put(key, value);
091
092        _keys.add(key);
093        _values.add(value);
094
095        _lastValue = value;
096    }
097
098    /**
099     * Returns a unmodifiable list of values stored into the converter, in the order in which they
100     * were stored.
101     * 
102     * @return an unmodifiable List
103     * @see #add(Object, Object)
104     */
105    public final List getAllValues()
106    {
107        return Collections.unmodifiableList(_values);
108    }
109
110    /**
111     * Returns a list of all values stored into the converter, with deleted values removed.
112     */
113
114    public final List getValues()
115    {
116        if (isDeletedValuesEmpty())
117            return getAllValues();
118
119        List result = new ArrayList(_values);
120
121        result.removeAll(_deletedValues);
122
123        return result;
124    }
125
126    /**
127     * Returns true if the deleted values set is empty (or null).l
128     */
129    private boolean isDeletedValuesEmpty()
130    {
131        return _deletedValues == null || _deletedValues.isEmpty();
132    }
133
134    /**
135     * Checks to see if the {@link #getLastValue() last value} is, or is not, in the set of deleted
136     * values.
137     */
138    public final boolean isDeleted()
139    {
140        return checkValueSetForLastValue(_deletedValues);
141    }
142
143    /**
144     * Checks the set to see if it contains the {@link #getLastValue() last value}.
145     * 
146     * @param valueSet
147     *            the set to check, which may be null
148     * @return true if the last value is in the set (if non-null)
149     */
150    protected final boolean checkValueSetForLastValue(Set valueSet)
151    {
152        return valueSet != null && valueSet.contains(_lastValue);
153    }
154
155    /**
156     * Adds or removes the {@link #getLastValue() last value} from the
157     * {@link #getDeletedValues() deleted values set}.
158     * 
159     * @param deleted
160     */
161    public final void setDeleted(boolean deleted)
162    {
163        _deletedValues = updateValueSetForLastValue(_deletedValues, deleted);
164    }
165
166    /**
167     * Updates a value set to add or remove the {@link #getLastValue() last value} to the set. The
168     * logic here will create and return a new Set instance if necessary (that is, if inSet is true
169     * and set is null). The point is to defer the creation of the set until its actually needed.
170     * 
171     * @param set
172     *            the set to update, which may be null
173     * @param inSet
174     *            if true, the last value will be added to the set (creating the set as necessary);
175     *            if false, the last value will be removed
176     * @return the set passed in, or a new Set instance
177     */
178    protected final Set updateValueSetForLastValue(Set set, boolean inSet)
179    {
180        if (inSet)
181        {
182            if (set == null)
183                set = new HashSet();
184
185            set.add(_lastValue);
186
187            return set;
188        }
189
190        if (set != null)
191            set.remove(_lastValue);
192
193        return set;
194    }
195
196    /**
197     * Returns the last active value; this is the value passed to {@link #getPrimaryKey(Object)} or
198     * the value for the key passed to {@link #getValue(Object)}.
199     * <p>
200     * Maintaining <em>value sets</em> involves adding or removing the active value from a set.
201     * 
202     * @return the last active object
203     */
204    public final Object getLastValue()
205    {
206        return _lastValue;
207    }
208
209    /**
210     * Returns an unmodifiable set of all values marked as deleted.
211     */
212
213    public final Set getDeletedValues()
214    {
215        return createUnmodifiableSet(_deletedValues);
216    }
217
218    /**
219     * Converts a value set into a returnable value; null is converted to the empty set, and
220     * non-null is wrapped as unmodifiable.
221     * 
222     * @param valueSet
223     *            the set to convert and return
224     * @return a non-null, non-modifiable Set
225     */
226    protected final Set createUnmodifiableSet(Set valueSet)
227    {
228        return valueSet == null ? Collections.EMPTY_SET : Collections.unmodifiableSet(valueSet);
229    }
230
231    /**
232     * Iterates over the keys and values, removing any values (and corresponding keys) that that are
233     * in the deleted set. After invoking this, {@link #getAllValues()} will be the same as
234     * {@link #getValues()}.
235     */
236    public final void removeDeletedValues()
237    {
238        _lastValue = null;
239
240        if (isDeletedValuesEmpty())
241            return;
242
243        int count = _keys.size();
244
245        for (int i = count - 1; i >= 0; i--)
246        {
247            if (_deletedValues.contains(_values.get(i)))
248            {
249                _values.remove(i);
250                Object key = _keys.remove(i);
251
252                _map.remove(key);
253            }
254        }
255    }
256
257    /**
258     * Gets the primary key of an object previously stored in this converter.
259     * 
260     * @param value
261     *            an object previously stored in the converter
262     * @return the corresponding key used to store the object
263     * @throws ApplicationRuntimeException
264     *             if the value was not previously stored
265     * @see #add(Object, Object)
266     */
267    public final Object getPrimaryKey(Object value)
268    {
269        int index = _values.indexOf(value);
270
271        if (index < 0)
272            throw new ApplicationRuntimeException(UtilMessages.valueNotFound(value), value, null,
273                    null);
274
275        _lastValue = value;
276
277        return _keys.get(index);
278    }
279
280    /**
281     * Given a primary key, locates the corresponding object. May invoke
282     * {@link #provideMissingValue(Object)} if no such key has been stored into the converter.
283     * 
284     * @return the object if the key is found, or null otherwise.
285     * @see #add(Object, Object)
286     */
287    public final Object getValue(Object primaryKey)
288    {
289        Object result = _map.get(primaryKey);
290
291        if (result == null)
292            result = provideMissingValue(primaryKey);
293
294        _lastValue = result;
295
296        return result;
297    }
298
299    /**
300     * Invoked by {@link #getValue(Object)} when the key is not found in the converter's map.
301     * Subclasses may override this method to either obtain the corresponding object from secondary
302     * storage, to throw an exception, or to provide a new object instance. This implementation
303     * returns null.
304     * 
305     * @param key
306     *            the key for which an object was requested
307     * @return the object for the key, or null if no object may can be provided
308     */
309    protected Object provideMissingValue(Object key)
310    {
311        return null;
312    }
313
314}