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            Set updatedSet = set;
179            if (value)
180            {
181                if (updatedSet == null)
182                    updatedSet = new HashSet();
183    
184                updatedSet.add(_currentKey);
185            }
186            else
187            {
188                if (updatedSet != null)
189                    updatedSet.remove(_currentKey);
190            }
191    
192            return updatedSet;
193        }
194    
195        /**
196         * Returns the deleted keys in an unspecified order. Returns a List, which may be empty if no
197         * keys have been deleted.
198         */
199    
200        public List getDeletedKeys()
201        {
202            return convertSetToList(_deletedKeys);
203        }
204    
205        /**
206         * Removes keys and values that are in the set of deleted keys, then clears the set of deleted
207         * keys. After invoking this method, {@link #getValues()} and {@link #getAllValues()} will
208         * return equivalent lists and {@link #getKeys()} will no longer show any of the deleted keys.
209         * Note that this method <em>does not</em> change the current key. Subclasses that track
210         * additional key sets may want to override this method to remove deleted keys from those key
211         * sets.
212         */
213    
214        public void purgeDeletedKeys()
215        {
216            if (_deletedKeys == null)
217                return;
218    
219            _map.keySet().removeAll(_deletedKeys);
220            _keys.removeAll(_deletedKeys);
221    
222            _deletedKeys = null;
223        }
224    
225        /**
226         * Invoked to convert a set into a List.
227         * 
228         * @param set
229         *            a set (which may be empty or null)
230         * @return a list (possibly empty) of the items in the set
231         */
232    
233        protected List convertSetToList(Set set)
234        {
235            if (Tapestry.isEmpty(set))
236                return Collections.EMPTY_LIST;
237    
238            return new ArrayList(set);
239        }
240    
241        /**
242         * Returns all the values stored in the map, in the order in which values were added to the map
243         * using {@link #add(Object, Object)}.
244         */
245    
246        public List getAllValues()
247        {
248            int count = _keys.size();
249            List result = new ArrayList(count);
250    
251            for (int i = 0; i < count; i++)
252            {
253                Object key = _keys.get(i);
254                Object value = _map.get(key);
255    
256                result.add(value);
257            }
258    
259            return result;
260        }
261    
262        /**
263         * Returns all the values stored in the map, excluding those whose id has been marked deleted,
264         * in the order in which values were added to the map using {@link #add(Object, Object)}.
265         */
266    
267        public List getValues()
268        {
269            int deletedCount = Tapestry.size(_deletedKeys);
270    
271            if (deletedCount == 0)
272                return getAllValues();
273    
274            int count = _keys.size();
275    
276            List result = new ArrayList(count - deletedCount);
277    
278            for (int i = 0; i < count; i++)
279            {
280                Object key = _keys.get(i);
281    
282                if (_deletedKeys.contains(key))
283                    continue;
284    
285                Object value = _map.get(key);
286                result.add(value);
287            }
288    
289            return result;
290        }
291    
292    }