001    /*
002     * Copyright 2002-2004 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.binding.value.support;
017    
018    import java.lang.reflect.Array;
019    import java.lang.reflect.Constructor;
020    import java.util.ArrayList;
021    import java.util.Arrays;
022    import java.util.Collection;
023    import java.util.Comparator;
024    import java.util.HashSet;
025    import java.util.Iterator;
026    import java.util.List;
027    import java.util.Set;
028    import java.util.SortedSet;
029    import java.util.TreeSet;
030    
031    import javax.swing.event.ListDataEvent;
032    import javax.swing.event.ListDataListener;
033    
034    import org.springframework.beans.BeanUtils;
035    import org.springframework.beans.FatalBeanException;
036    import org.springframework.binding.value.ValueModel;
037    import org.springframework.richclient.util.ClassUtils;
038    import org.springframework.util.Assert;
039    
040    /**
041     * A <code>BufferedValueModel</code> that uses an ObservableList as a buffer to hold
042     * chandes to a <code>Collection</code> or <code>array</code>. Internally this is
043     * called the "buffered list model."
044     * <p>
045     * On commit the following steps occur:
046     * <ol>
047     * <li>a new instance of the backing collection type is created</li>
048     * <li>the contents of the list model is inserted into this new collection</li>
049     * <li>the new collection is saved into the underlying collection's value model</li>
050     * <li>the structure of the list model is compared to the structure of the new underlying
051     * collection and if they differ the list model is updated to reflect the new structure.</li>
052     * </ol>
053     * <p>
054     * NOTE: Between calls to commit the list model adheres to the contract defined in
055     * <code>java.util.List</code> NOT the contract of the underlying collection's type.
056     * This can result in the list model representing a state that is not possible for the
057     * underlying collection.
058     * 
059     * 
060     * @author oliverh
061     */
062    public class BufferedCollectionValueModel extends BufferedValueModel {
063    
064        private final ListChangeHandler listChangeHandler = new ListChangeHandler();
065    
066        private final Class wrappedType;
067    
068        private final Class wrappedConcreteType;
069    
070        private ObservableList bufferedListModel;
071    
072        /**
073         * Constructs a new BufferedCollectionValueModel.
074         * 
075         * @param wrappedModel the value model to wrap
076         * @param wrappedType the class of the value contained by wrappedModel; this must be
077         *            assignable to <code>java.util.Collection</code> or
078         *            <code>Object[]</code>.
079         */
080        public BufferedCollectionValueModel(ValueModel wrappedModel, Class wrappedType) {
081            super(wrappedModel);
082            Assert.notNull(wrappedType);
083            this.wrappedType = wrappedType;
084            this.wrappedConcreteType = getConcreteCollectionType(wrappedType);
085    
086            updateBufferedListModel(getWrappedValue());
087            if (getValue() != bufferedListModel) {
088                super.setValue(bufferedListModel);
089            }
090        }
091    
092        public void setValue(Object value) {
093            if (value != bufferedListModel) {
094                if (!hasSameStructure()) {
095                    updateBufferedListModel(value);
096                    fireValueChange(bufferedListModel, bufferedListModel);
097                }
098            }
099        }
100    
101        protected Object getValueToCommit() {
102            Object wrappedValue = getWrappedValue();
103            // If the wrappedValue is null and the buffer is empty 
104            // just return null rather than an empty collection
105            if (wrappedValue == null && bufferedListModel.size() == 0)
106                return null;
107    
108            return createCollection(wrappedValue);
109        }
110    
111        //    protected void doBufferedValueCommit(Object bufferedValue) {
112        //        if (hasSameStructure()) {
113        //            return;
114        //        }
115        //        getWrappedValueModel().setValue(createCollection());
116        //        if (hasSameStructure()) {
117        //            return;
118        //        }
119        //        updateListModel(getWrappedValue());
120        //    }
121    
122        public static Class getConcreteCollectionType(Class wrappedType) {
123            Class class2Create;
124            if (wrappedType.isArray()) {
125                if (ClassUtils.isPrimitiveArray(wrappedType)) {
126                    throw new IllegalArgumentException("wrappedType can not be an array of primitive types");
127                }
128                class2Create = wrappedType;
129            }
130            else if (wrappedType == Collection.class) {
131                class2Create = ArrayList.class;
132            }
133            else if (wrappedType == List.class) {
134                class2Create = ArrayList.class;
135            }
136            else if (wrappedType == Set.class) {
137                class2Create = HashSet.class;
138            }
139            else if (wrappedType == SortedSet.class) {
140                class2Create = TreeSet.class;
141            }
142            else if (Collection.class.isAssignableFrom(wrappedType)) {
143                if (wrappedType.isInterface()) {
144                    throw new IllegalArgumentException("unable to handle Collection of type [" + wrappedType
145                            + "]. Do not know how to create a concrete implementation");
146                }
147                class2Create = wrappedType;
148            }
149            else {
150                throw new IllegalArgumentException("wrappedType [" + wrappedType + "] must be an array or a Collection");
151            }
152            return class2Create;
153        }
154    
155        /**
156         * Checks if the structure of the buffered list model is the same as the wrapped
157         * collection. "same structure" is defined as having the same elements in the
158         * same order with the one exception that NULL == empty list.
159         */
160        private boolean hasSameStructure() {
161            Object wrappedCollection = getWrappedValue();
162            if (wrappedCollection == null) {
163                return bufferedListModel.size() == 0;
164            }
165            else if (wrappedCollection instanceof Object[]) {
166                Object[] wrappedArray = (Object[])wrappedCollection;
167                if (wrappedArray.length != bufferedListModel.size()) {
168                    return false;
169                }
170                for (int i = 0; i < bufferedListModel.size(); i++) {
171                    if(super.hasValueChanged(wrappedArray[i], bufferedListModel.get(i))) {
172                        return false;
173                    }
174                }
175            }
176            else {
177                if (((Collection)wrappedCollection).size() != bufferedListModel.size()) {
178                    return false;
179                }
180                for (Iterator i = ((Collection)wrappedCollection).iterator(), j = bufferedListModel.iterator(); i.hasNext();) {
181                    if (super.hasValueChanged(i.next(), j.next())) {
182                        return false;
183                    }
184                }
185            }
186            return true;
187        }
188    
189        private Object createCollection(Object wrappedCollection) {
190            return populateFromListModel(createNewCollection(wrappedCollection));
191        }
192    
193        private Object createNewCollection(Object wrappedCollection) {
194            if (wrappedConcreteType.isArray())
195                return Array.newInstance(wrappedConcreteType.getComponentType(), bufferedListModel.size());
196    
197            Object newCollection;
198            if (SortedSet.class.isAssignableFrom(wrappedConcreteType) && wrappedCollection instanceof SortedSet
199                    && ((SortedSet)wrappedCollection).comparator() != null) {
200                try {
201                    Constructor con = wrappedConcreteType.getConstructor(new Class[] {Comparator.class});
202                    newCollection = BeanUtils.instantiateClass(con,
203                            new Object[] {((SortedSet)wrappedCollection).comparator()});
204                }
205                catch (NoSuchMethodException e) {
206                    throw new FatalBeanException("Could not instantiate SortedSet class ["
207                            + wrappedConcreteType.getName() + "]: no constructor taking Comparator found", e);
208                }
209            }
210            else {
211                newCollection = BeanUtils.instantiateClass(wrappedConcreteType);
212            }
213            return newCollection;
214        }
215    
216        private Object populateFromListModel(Object collection) {
217            if (collection instanceof Object[]) {
218                Object[] wrappedArray = (Object[])collection;
219                for (int i = 0; i < bufferedListModel.size(); i++) {
220                    wrappedArray[i] = bufferedListModel.get(i);
221                }
222            }
223            else {
224                Collection wrappedCollection = ((Collection)collection);
225                wrappedCollection.clear();
226                wrappedCollection.addAll(bufferedListModel);
227            }
228            return collection;
229        }
230    
231        /**
232         * Create an empty buffered list model. May be overridden to provide specialized
233         * implementations.
234         * @return ObservableList to use for buffered value. This default uses an instance of
235         *         ListListModel.
236         */
237        protected ObservableList createBufferedListModel() {
238            return new ListListModel();
239        }
240    
241        /**
242         * Gets the list value associated with this value model, creating a list
243         * model buffer containing its contents, suitable for manipulation.
244         * 
245         * @return The list model buffer
246         */
247        private Object updateBufferedListModel(final Object wrappedCollection) {
248            if (bufferedListModel == null) {
249                bufferedListModel = createBufferedListModel();
250                bufferedListModel.addListDataListener(listChangeHandler);
251                setValue(bufferedListModel);
252            }
253            if (wrappedCollection == null) {
254                bufferedListModel.clear();
255            }
256            else {
257                if (wrappedType.isAssignableFrom(wrappedCollection.getClass())) {
258                    Collection buffer = null;
259                    if (wrappedCollection instanceof Object[]) {
260                        Object[] wrappedArray = (Object[])wrappedCollection;
261                        buffer = Arrays.asList(wrappedArray);
262                    }
263                    else {
264                        buffer = (Collection)wrappedCollection;
265                    }
266                    bufferedListModel.clear();
267                    bufferedListModel.addAll(prepareBackingCollection(buffer));
268                }
269                else {
270                    throw new IllegalArgumentException("wrappedCollection must be assignable from " + wrappedType.getName());
271                }
272    
273            }
274            return bufferedListModel;
275        }
276    
277        /**
278         * Prepare the backing collection for installation into the buffered list model.  The default
279         * implementation of this method simply returns it.  Subclasses can do whatever is needed
280         * to the elements of the colleciton (or the collection itself).  For example, the
281         * elements might be cloned or wrapped in a an adapter.
282         * @param col The collection of objects to process
283         * @return processed collection
284         */
285        protected Collection prepareBackingCollection(Collection col) {
286            return col;
287        }
288    
289        private Object getWrappedValue() {
290            return getWrappedValueModel().getValue();
291        }
292    
293        protected void fireListModelChanged() {
294            if (isBuffering()) {
295                super.fireValueChange(bufferedListModel, bufferedListModel);
296            }
297            else {
298                super.setValue(bufferedListModel);
299            }
300        }
301    
302        protected boolean hasValueChanged(Object oldValue, Object newValue) {
303            return (oldValue == bufferedListModel && newValue == bufferedListModel) || super.hasValueChanged(oldValue, newValue);
304        }
305    
306        private class ListChangeHandler implements ListDataListener {
307            public void contentsChanged(ListDataEvent e) {
308                fireListModelChanged();
309            }
310    
311            public void intervalAdded(ListDataEvent e) {
312                fireListModelChanged();
313            }
314    
315            public void intervalRemoved(ListDataEvent e) {
316                fireListModelChanged();
317            }
318        }
319    }