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 }