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
015 package org.apache.tapestry.util;
016
017 import java.util.ArrayList;
018 import java.util.Collections;
019 import java.util.HashMap;
020 import java.util.HashSet;
021 import java.util.List;
022 import java.util.Map;
023 import java.util.Set;
024
025 import org.apache.hivemind.ApplicationRuntimeException;
026 import org.apache.hivemind.util.Defense;
027 import 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 */
053 public 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).
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 Set updatedSet = set;
181 if (inSet)
182 {
183 if (updatedSet == null)
184 updatedSet = new HashSet();
185
186 updatedSet.add(_lastValue);
187
188 return updatedSet;
189 }
190
191 if (updatedSet != null)
192 updatedSet.remove(_lastValue);
193
194 return updatedSet;
195 }
196
197 /**
198 * Returns the last active value; this is the value passed to {@link #getPrimaryKey(Object)} or
199 * the value for the key passed to {@link #getValue(Object)}.
200 * <p>
201 * Maintaining <em>value sets</em> involves adding or removing the active value from a set.
202 *
203 * @return the last active object
204 */
205 public final Object getLastValue()
206 {
207 return _lastValue;
208 }
209
210 /**
211 * Returns an unmodifiable set of all values marked as deleted.
212 */
213
214 public final Set getDeletedValues()
215 {
216 return createUnmodifiableSet(_deletedValues);
217 }
218
219 /**
220 * Converts a value set into a returnable value; null is converted to the empty set, and
221 * non-null is wrapped as unmodifiable.
222 *
223 * @param valueSet
224 * the set to convert and return
225 * @return a non-null, non-modifiable Set
226 */
227 protected final Set createUnmodifiableSet(Set valueSet)
228 {
229 return valueSet == null ? Collections.EMPTY_SET : Collections.unmodifiableSet(valueSet);
230 }
231
232 /**
233 * Iterates over the keys and values, removing any values (and corresponding keys) that that are
234 * in the deleted set. After invoking this, {@link #getAllValues()} will be the same as
235 * {@link #getValues()}.
236 */
237 public final void removeDeletedValues()
238 {
239 _lastValue = null;
240
241 if (isDeletedValuesEmpty())
242 return;
243
244 int count = _keys.size();
245
246 for (int i = count - 1; i >= 0; i--)
247 {
248 if (_deletedValues.contains(_values.get(i)))
249 {
250 _values.remove(i);
251 Object key = _keys.remove(i);
252
253 _map.remove(key);
254 }
255 }
256 }
257
258 /**
259 * Gets the primary key of an object previously stored in this converter.
260 *
261 * @param value
262 * an object previously stored in the converter
263 * @return the corresponding key used to store the object
264 * @throws ApplicationRuntimeException
265 * if the value was not previously stored
266 * @see #add(Object, Object)
267 */
268 public final Object getPrimaryKey(Object value)
269 {
270 int index = _values.indexOf(value);
271
272 if (index < 0)
273 throw new ApplicationRuntimeException(UtilMessages.valueNotFound(value), value, null,
274 null);
275
276 _lastValue = value;
277
278 return _keys.get(index);
279 }
280
281 /**
282 * Given a primary key, locates the corresponding object. May invoke
283 * {@link #provideMissingValue(Object)} if no such key has been stored into the converter.
284 *
285 * @return the object if the key is found, or null otherwise.
286 * @see #add(Object, Object)
287 */
288 public final Object getValue(Object primaryKey)
289 {
290 Object result = _map.get(primaryKey);
291
292 if (result == null)
293 result = provideMissingValue(primaryKey);
294
295 _lastValue = result;
296
297 return result;
298 }
299
300 /**
301 * Invoked by {@link #getValue(Object)} when the key is not found in the converter's map.
302 * Subclasses may override this method to either obtain the corresponding object from secondary
303 * storage, to throw an exception, or to provide a new object instance. This implementation
304 * returns null.
305 *
306 * @param key
307 * the key for which an object was requested
308 * @return the object for the key, or null if no object may can be provided
309 */
310 protected Object provideMissingValue(Object key)
311 {
312 return null;
313 }
314
315 }