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 }