/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.binding.value.support;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.FatalBeanException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.valkyriercp.binding.value.ObservableList;
import org.valkyriercp.binding.value.ValueModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.*;
/**
* A <code>BufferedValueModel</code> that uses an ObservableList as a buffer to hold
* chandes to a <code>Collection</code> or <code>array</code>. Internally this is
* called the "buffered list model."
* <p>
* On commit the following steps occur:
* <ol>
* <li>a new instance of the backing collection type is created</li>
* <li>the contents of the list model is inserted into this new collection</li>
* <li>the new collection is saved into the underlying collection's value model</li>
* <li>the structure of the list model is compared to the structure of the new underlying
* collection and if they differ the list model is updated to reflect the new structure.</li>
* </ol>
* <p>
* NOTE: Between calls to commit the list model adheres to the contract defined in
* <code>java.util.List</code> NOT the contract of the underlying collection's type.
* This can result in the list model representing a state that is not possible for the
* underlying collection.
*
*
* @author oliverh
*/
public class BufferedCollectionValueModel extends BufferedValueModel {
private final ListChangeHandler listChangeHandler = new ListChangeHandler();
private final Class wrappedType;
private final Class wrappedConcreteType;
private ObservableList bufferedListModel;
/**
* Constructs a new BufferedCollectionValueModel.
*
* @param wrappedModel the value model to wrap
* @param wrappedType the class of the value contained by wrappedModel; this must be
* assignable to <code>java.util.Collection</code> or
* <code>Object[]</code>.
*/
public BufferedCollectionValueModel(ValueModel wrappedModel, Class wrappedType) {
super(wrappedModel);
Assert.notNull(wrappedType);
this.wrappedType = wrappedType;
this.wrappedConcreteType = getConcreteCollectionType(wrappedType);
updateBufferedListModel(getWrappedValue());
if (getValue() != bufferedListModel) {
super.setValue(bufferedListModel);
}
}
public void setValue(Object value) {
if (value != bufferedListModel) {
if (!hasSameStructure()) {
updateBufferedListModel(value);
fireValueChange(bufferedListModel, bufferedListModel);
}
}
}
protected Object getValueToCommit() {
Object wrappedValue = getWrappedValue();
// If the wrappedValue is null and the buffer is empty
// just return null rather than an empty collection
if (wrappedValue == null && bufferedListModel.size() == 0)
return null;
return createCollection(wrappedValue);
}
// protected void doBufferedValueCommit(Object bufferedValue) {
// if (hasSameStructure()) {
// return;
// }
// getWrappedValueModel().setValue(createCollection());
// if (hasSameStructure()) {
// return;
// }
// updateListModel(getWrappedValue());
// }
public static Class getConcreteCollectionType(Class wrappedType) {
Class class2Create;
if (wrappedType.isArray()) {
if (ClassUtils.isPrimitiveArray(wrappedType)) {
throw new IllegalArgumentException("wrappedType can not be an array of primitive types");
}
class2Create = wrappedType;
}
else if (wrappedType == Collection.class) {
class2Create = ArrayList.class;
}
else if (wrappedType == List.class) {
class2Create = ArrayList.class;
}
else if (wrappedType == Set.class) {
class2Create = HashSet.class;
}
else if (wrappedType == SortedSet.class) {
class2Create = TreeSet.class;
}
else if (Collection.class.isAssignableFrom(wrappedType)) {
if (wrappedType.isInterface()) {
throw new IllegalArgumentException("unable to handle Collection of type [" + wrappedType
+ "]. Do not know how to create a concrete implementation");
}
class2Create = wrappedType;
}
else {
throw new IllegalArgumentException("wrappedType [" + wrappedType + "] must be an array or a Collection");
}
return class2Create;
}
/**
* Checks if the structure of the buffered list model is the same as the wrapped
* collection. "same structure" is defined as having the same elements in the
* same order with the one exception that NULL == empty list.
*/
private boolean hasSameStructure() {
Object wrappedCollection = getWrappedValue();
if (wrappedCollection == null) {
return bufferedListModel.size() == 0;
}
else if (wrappedCollection instanceof Object[]) {
Object[] wrappedArray = (Object[])wrappedCollection;
if (wrappedArray.length != bufferedListModel.size()) {
return false;
}
for (int i = 0; i < bufferedListModel.size(); i++) {
if(super.hasValueChanged(wrappedArray[i], bufferedListModel.get(i))) {
return false;
}
}
}
else {
if (((Collection)wrappedCollection).size() != bufferedListModel.size()) {
return false;
}
for (Iterator i = ((Collection)wrappedCollection).iterator(), j = bufferedListModel.iterator(); i.hasNext();) {
if (super.hasValueChanged(i.next(), j.next())) {
return false;
}
}
}
return true;
}
private Object createCollection(Object wrappedCollection) {
return populateFromListModel(createNewCollection(wrappedCollection));
}
private Object createNewCollection(Object wrappedCollection) {
if (wrappedConcreteType.isArray())
return Array.newInstance(wrappedConcreteType.getComponentType(), bufferedListModel.size());
Object newCollection;
if (SortedSet.class.isAssignableFrom(wrappedConcreteType) && wrappedCollection instanceof SortedSet
&& ((SortedSet)wrappedCollection).comparator() != null) {
try {
Constructor con = wrappedConcreteType.getConstructor(new Class[] {Comparator.class});
newCollection = BeanUtils.instantiateClass(con,
new Object[]{((SortedSet) wrappedCollection).comparator()});
}
catch (NoSuchMethodException e) {
throw new FatalBeanException("Could not instantiate SortedSet class ["
+ wrappedConcreteType.getName() + "]: no constructor taking Comparator found", e);
}
}
else {
newCollection = BeanUtils.instantiateClass(wrappedConcreteType);
}
return newCollection;
}
private Object populateFromListModel(Object collection) {
if (collection instanceof Object[]) {
Object[] wrappedArray = (Object[])collection;
for (int i = 0; i < bufferedListModel.size(); i++) {
wrappedArray[i] = bufferedListModel.get(i);
}
}
else {
Collection wrappedCollection = ((Collection)collection);
wrappedCollection.clear();
wrappedCollection.addAll(bufferedListModel);
}
return collection;
}
/**
* Create an empty buffered list model. May be overridden to provide specialized
* implementations.
* @return ObservableList to use for buffered value. This default uses an instance of
* ListListModel.
*/
protected ObservableList createBufferedListModel() {
return new ListListModel();
}
/**
* Gets the list value associated with this value model, creating a list
* model buffer containing its contents, suitable for manipulation.
*
* @return The list model buffer
*/
private Object updateBufferedListModel(final Object wrappedCollection) {
if (bufferedListModel == null) {
bufferedListModel = createBufferedListModel();
bufferedListModel.addListDataListener(listChangeHandler);
setValue(bufferedListModel);
}
if (wrappedCollection == null) {
bufferedListModel.clear();
}
else {
if (wrappedType.isAssignableFrom(wrappedCollection.getClass())) {
Collection buffer = null;
if (wrappedCollection instanceof Object[]) {
Object[] wrappedArray = (Object[])wrappedCollection;
buffer = Arrays.asList(wrappedArray);
}
else {
buffer = (Collection)wrappedCollection;
}
bufferedListModel.clear();
bufferedListModel.addAll(prepareBackingCollection(buffer));
}
else {
throw new IllegalArgumentException("wrappedCollection must be assignable from " + wrappedType.getName());
}
}
return bufferedListModel;
}
/**
* Prepare the backing collection for installation into the buffered list model. The default
* implementation of this method simply returns it. Subclasses can do whatever is needed
* to the elements of the colleciton (or the collection itself). For example, the
* elements might be cloned or wrapped in a an adapter.
* @param col The collection of objects to process
* @return processed collection
*/
protected Collection prepareBackingCollection(Collection col) {
return col;
}
private Object getWrappedValue() {
return getWrappedValueModel().getValue();
}
protected void fireListModelChanged() {
if (isBuffering()) {
super.fireValueChange(bufferedListModel, bufferedListModel);
}
else {
super.setValue(bufferedListModel);
}
}
protected boolean hasValueChanged(Object oldValue, Object newValue) {
return (oldValue == bufferedListModel && newValue == bufferedListModel) || super.hasValueChanged(oldValue, newValue);
}
private class ListChangeHandler implements ListDataListener {
public void contentsChanged(ListDataEvent e) {
fireListModelChanged();
}
public void intervalAdded(ListDataEvent e) {
fireListModelChanged();
}
public void intervalRemoved(ListDataEvent e) {
fireListModelChanged();
}
}
}