001 // Copyright 2004, 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.form;
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.tapestry.Tapestry;
026
027 /**
028 * A utility class often used with the {@link org.apache.tapestry.form.ListEdit} component. A
029 * ListEditMap is loaded with data objects before the ListEdit renders, and again before the
030 * ListEdit rewinds. This streamlines the synchronization of the form against data on the server. It
031 * is most useful when the set of objects is of a manageable size (say, no more than a few hundred
032 * objects).
033 * <p>
034 * The map stores a list of keys, and relates each key to a value. It also tracks a deleted flag for
035 * each key.
036 * <p>
037 * Usage: <br>
038 * The page or component should implement {@link org.apache.tapestry.event.PageBeginRenderListener}
039 * and implement
040 * {@link org.apache.tapestry.event.PageBeginRenderListener#pageBeginRender(org.apache.tapestry.event.PageEvent)}
041 * to initialize the map.
042 * <p>
043 * The external data (from which keys and values are obtained) is queried, and each key/value pair
044 * is {@link #add(Object, Object) added} to the map, in the order that items should be presented.
045 * <p>
046 * The {@link org.apache.tapestry.form.ListEdit}'s source parameter should be bound to the map's
047 * {@link #getKeys() keys} property. The value parameter should be bound to the map's
048 * {@link #setKey(Object) key} property.
049 * <p>
050 * The {@link org.apache.tapestry.form.ListEdit}'s listener parameter should be bound to a listener
051 * method to synchronize a property of the component from the map. <code>
052 * public void synchronize()
053 * {
054 * ListEditMap map = ...;
055 * <i>Type</i> object = (<i>Type</i>)map.getValue();
056 *
057 * if (object == null)
058 * ...
059 *
060 * set<i>Property</i>(object);
061 * }
062 * </code>
063 * <p>
064 * You may also connect a {@link org.apache.tapestry.form.Checkbox}'s selected parameter to the
065 * map's {@link #isDeleted() deleted} property.
066 * <p>
067 * You may track inclusion in other sets by subclassing ListEditMap and implementing new boolean
068 * properties. The accessor method should be a call to {@link #checkSet(Set)} and the mutator method
069 * should be a call to {@link #updateSet(Set, boolean)}.
070 *
071 * @author Howard Lewis Ship
072 * @since 3.0
073 */
074
075 public class ListEditMap
076 {
077 private Map _map = new HashMap();
078
079 private List _keys = new ArrayList();
080
081 private Set _deletedKeys;
082
083 private Object _currentKey;
084
085 /**
086 * Records the key and value into this map. The keys may be obtained, in the order in which they
087 * are added, using {@link #getKeys()}. This also sets the current key (so that you may invoke
088 * {@link #setDeleted(boolean)}, for example).
089 */
090
091 public void add(Object key, Object value)
092 {
093 _currentKey = key;
094
095 _keys.add(_currentKey);
096 _map.put(_currentKey, value);
097 }
098
099 /**
100 * Returns a List of keys, in the order that keys were added to the map (using
101 * {@link #add(Object, Object)}. The caller must not modify the List.
102 */
103
104 public List getKeys()
105 {
106 return _keys;
107 }
108
109 /**
110 * Sets the key for the map. This defines the key used with the other methods:
111 * {@link #getValue()}, {@link #isDeleted()}, {@link #setDeleted(boolean)}.
112 */
113
114 public void setKey(Object key)
115 {
116 _currentKey = key;
117 }
118
119 /**
120 * Returns the current key within the map.
121 */
122
123 public Object getKey()
124 {
125 return _currentKey;
126 }
127
128 /**
129 * Returns the value for the key (set using {@link #setKey(Object)}). May return null if no
130 * such key has been added (this can occur if a data object is deleted between the time a form
131 * is rendered and the time a form is submitted).
132 */
133
134 public Object getValue()
135 {
136 return _map.get(_currentKey);
137 }
138
139 /**
140 * Returns true if the {@link #setKey(Object) current key} is in the set of deleted keys.
141 */
142
143 public boolean isDeleted()
144 {
145 return checkSet(_deletedKeys);
146 }
147
148 /**
149 * Returns true if the set contains the {@link #getKey() current key}. Returns false is the set
150 * is null, or doesn't contain the current key.
151 */
152
153 protected boolean checkSet(Set set)
154 {
155 if (set == null)
156 return false;
157
158 return set.contains(_currentKey);
159 }
160
161 /**
162 * Adds or removes the {@link #setKey(Object) current key} from the set of deleted keys.
163 */
164
165 public void setDeleted(boolean value)
166 {
167 _deletedKeys = updateSet(_deletedKeys, value);
168 }
169
170 /**
171 * Updates the set, adding or removing the {@link #getKey() current key} from it. Returns the
172 * set passed in. If the value is true and the set is null, an new instance of {@link HashSet}
173 * is created and returned.
174 */
175
176 protected Set updateSet(Set set, boolean value)
177 {
178 if (value)
179 {
180 if (set == null)
181 set = new HashSet();
182
183 set.add(_currentKey);
184 }
185 else
186 {
187 if (set != null)
188 set.remove(_currentKey);
189 }
190
191 return set;
192 }
193
194 /**
195 * Returns the deleted keys in an unspecified order. Returns a List, which may be empty if no
196 * keys have been deleted.
197 */
198
199 public List getDeletedKeys()
200 {
201 return convertSetToList(_deletedKeys);
202 }
203
204 /**
205 * Removes keys and values that are in the set of deleted keys, then clears the set of deleted
206 * keys. After invoking this method, {@link #getValues()} and {@link #getAllValues()} will
207 * return equivalent lists and {@link #getKeys()} will no longer show any of the deleted keys.
208 * Note that this method <em>does not</em> change the current key. Subclasses that track
209 * additional key sets may want to override this method to remove deleted keys from those key
210 * sets.
211 */
212
213 public void purgeDeletedKeys()
214 {
215 if (_deletedKeys == null)
216 return;
217
218 _map.keySet().removeAll(_deletedKeys);
219 _keys.removeAll(_deletedKeys);
220
221 _deletedKeys = null;
222 }
223
224 /**
225 * Invoked to convert a set into a List.
226 *
227 * @param set
228 * a set (which may be empty or null)
229 * @return a list (possibly empty) of the items in the set
230 */
231
232 protected List convertSetToList(Set set)
233 {
234 if (Tapestry.isEmpty(set))
235 return Collections.EMPTY_LIST;
236
237 return new ArrayList(set);
238 }
239
240 /**
241 * Returns all the values stored in the map, in the order in which values were added to the map
242 * using {@link #add(Object, Object)}.
243 */
244
245 public List getAllValues()
246 {
247 int count = _keys.size();
248 List result = new ArrayList(count);
249
250 for (int i = 0; i < count; i++)
251 {
252 Object key = _keys.get(i);
253 Object value = _map.get(key);
254
255 result.add(value);
256 }
257
258 return result;
259 }
260
261 /**
262 * Returns all the values stored in the map, excluding those whose id has been marked deleted,
263 * in the order in which values were added to the map using {@link #add(Object, Object)}.
264 */
265
266 public List getValues()
267 {
268 int deletedCount = Tapestry.size(_deletedKeys);
269
270 if (deletedCount == 0)
271 return getAllValues();
272
273 int count = _keys.size();
274
275 List result = new ArrayList(count - deletedCount);
276
277 for (int i = 0; i < count; i++)
278 {
279 Object key = _keys.get(i);
280
281 if (_deletedKeys.contains(key))
282 continue;
283
284 Object value = _map.get(key);
285 result.add(value);
286 }
287
288 return result;
289 }
290
291 }