/*
* Copyright 2009 Andrew Pietsch
*
* 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 com.dragome.forms.bindings.client.bean;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import com.dragome.forms.bindings.client.list.ArrayListModel;
import com.dragome.forms.bindings.client.value.ValueHolder;
import com.dragome.forms.bindings.client.value.ValueModel;
import com.dragome.model.interfaces.ValueChangeEvent;
import com.dragome.model.interfaces.ValueChangeHandler;
/**
*
*/
public class BeanPropertyListModel<T> extends ArrayListModel<T> implements BeanPropertyModelBase
{
private List<T> EMPTY_LIST= Collections.emptyList();
private List<T> checkpointValue= null;
private CollectionConverter listConverter;
private ValueHolder<Boolean> dirtyModel= new ValueHolder<Boolean>(false);
private ValueHolder<Boolean> mutableModel= new ValueHolder<Boolean>(false);
private ValueModel<?> source;
private ValueModel<Boolean> autoCommit;
private UpdateStrategy<T> defaultUpdateStrategy= new DefaultUpdateStrategy();
private UpdateStrategy<T> autoCommitUpdateStrategy= new AutoCommitUpdateStrategy();
private PropertyDescriptor propertyDescriptor;
public BeanPropertyListModel(ValueModel<?> sourceModel, PropertyDescriptor descriptor, CollectionConverter converter, ValueModel<Boolean> autoCommit)
{
this.source= sourceModel;
this.propertyDescriptor= descriptor;
this.listConverter= converter;
this.autoCommit= autoCommit;
dirtyModel.setFireEventsEvenWhenValuesEqual(false);
installValueChangeHandler();
handleSourceModelChange();
}
@SuppressWarnings("unchecked")
private void installValueChangeHandler()
{
// yep I know, no generics... I don't know or care what the type is since the accessor handles
// all that. And since ValueChangeHandler doesn't allow for ValueChangeHander<? super T> I can't
// add a ValueChangeHandler<Object> to a ValueModel<?>
this.source.addValueChangeHandler(new ValueChangeHandler()
{
public void onValueChange(ValueChangeEvent bValueChangeEvent)
{
handleSourceModelChange();
}
});
}
private void handleSourceModelChange()
{
readFromSource();
onSourceModelChanged(source.getValue());
}
/**
* This is an empty method that subclasses can override to perform
* actions when the source bean changes.
*
* @param sourceBean the new value of the source bean.
*/
protected void onSourceModelChanged(Object sourceBean)
{
}
public String getPropertyName()
{
return propertyDescriptor.getPropertyName();
}
public Class getValueType()
{
return propertyDescriptor.getElementType();
}
protected boolean isAutoCommit()
{
// only true if not null and true.
return Boolean.TRUE.equals(autoCommit.getValue());
}
private void ensureMutable()
{
if (!isMutableProperty())
{
throw new ReadOnlyPropertyException(propertyDescriptor);
}
else if (!isNonNullSource())
{
throw new SourceBeanIsNullException(propertyDescriptor);
}
}
@SuppressWarnings("unchecked")
public void readFromSource()
{
getUpdateStrategy().readFromSource();
}
public void writeToSource(boolean checkpoint)
{
getUpdateStrategy().writeToSource(checkpoint);
}
@Override
public void setElements(Collection<? extends T> elements)
{
getUpdateStrategy().setElements(elements);
}
@Override
public void clear()
{
getUpdateStrategy().clear();
}
@Override
public void remove(T element)
{
getUpdateStrategy().remove(element);
}
@Override
public void add(T element)
{
getUpdateStrategy().add(element);
}
/**
* @deprecated use {@link #dirty()} instead.
*/
@Deprecated
public ValueModel<Boolean> getDirtyModel()
{
return dirty();
}
public ValueModel<Boolean> dirty()
{
return dirtyModel;
}
/**
* Checkpoints the models dirty state to the current value of the model. After calling this
* method the dirty state will be <code>false</code>.
*
* @see #revert()
*/
public void checkpoint()
{
getUpdateStrategy().checkpoint();
}
/**
* Reverts the value of this model to the previous checkpoint. If checkpoint hasn't been called
* then it will revert to the last call to readFrom.
*/
public void revert()
{
getUpdateStrategy().revert();
}
public ValueModel<Boolean> getMutableModel()
{
return mutableModel;
}
public boolean isMutable()
{
return getMutableModel().getValue();
}
private void updateMutableState()
{
mutableModel.setValue(isMutableProperty() && isNonNullSource());
}
private boolean isNonNullSource()
{
return source.getValue() != null;
}
public boolean isMutableProperty()
{
return propertyDescriptor.isMutable();
}
private UpdateStrategy<T> getUpdateStrategy()
{
return isAutoCommit() ? autoCommitUpdateStrategy : defaultUpdateStrategy;
}
private interface UpdateStrategy<T>
{
void readFromSource();
void writeToSource(boolean checkpoint);
void setElements(Collection<? extends T> elements);
void add(T value);
void remove(T value);
void clear();
void checkpoint();
void revert();
}
public class DefaultUpdateStrategy implements UpdateStrategy<T>
{
@SuppressWarnings("unchecked")
public void readFromSource()
{
Object propertyValue= propertyDescriptor.readProperty(source.getValue());
updateElements(toList(listConverter.fromBean(propertyValue)));
checkpoint();
updateMutableState();
afterMutate();
}
public void writeToSource(boolean checkpoint)
{
ensureMutable();
propertyDescriptor.writeProperty(source.getValue(), listConverter.toBean(asUnmodifiableList()));
if (checkpoint)
{
checkpoint();
}
}
private void updateElements(Collection<? extends T> elements)
{
BeanPropertyListModel.super.setElements(elements);
}
public void setElements(Collection<? extends T> elements)
{
ensureMutable();
updateElements(elements);
afterMutate();
}
public void add(T element)
{
ensureMutable();
BeanPropertyListModel.super.add(element);
afterMutate();
}
public void remove(T element)
{
ensureMutable();
BeanPropertyListModel.super.remove(element);
afterMutate();
}
public void clear()
{
ensureMutable();
BeanPropertyListModel.super.clear();
afterMutate();
}
protected void afterMutate()
{
updateDirtyState();
}
public void checkpoint()
{
// we copy so mutations don't affect us.
checkpointValue= new ArrayList<T>(asUnmodifiableList());
dirtyModel.setValue(false);
}
public void revert()
{
// setElements makes a copy of the data (i.e it doesn't maintain a reference
// to the list so we don't need copy it passing in).
setElements(getCheckpoint());
}
/**
* Returns the last checkpoint value.
*
* @return the checkpoint value.
*/
private List<T> getCheckpoint()
{
return checkpointValue != null ? checkpointValue : EMPTY_LIST;
}
/**
* Always returns a non-null list from the specified collection. If the collection is
* and instance of List the the original list is returned, if null then EMPTY_LIST is returned,
* otherwise a new ArrayList containing the original collections elements is returned.
*
* @param collection the collection.
* @return the collection if it is a list, EMPTY_LIST if the collection is null, or a new
* ArrayList containing the elements of the collection.
*/
private List<T> toList(Collection<T> collection)
{
if (collection instanceof List)
{
return (List<T>) collection;
}
else
{
return collection != null ? new ArrayList<T>(collection) : EMPTY_LIST;
}
}
void updateDirtyState()
{
dirtyModel.setValue(computeDirty());
}
protected boolean computeDirty()
{
if (checkpointValue == null)
{
return size() != 0;
}
else if (size() != checkpointValue.size())
{
return true;
}
else
{
// the sizes are equal so we check the contents are the same.
for (int i= 0; i < checkpointValue.size(); i++)
{
if (!areEqual(get(i), checkpointValue.get(i)))
{
return true;
}
}
return false;
}
}
}
private class AutoCommitUpdateStrategy extends DefaultUpdateStrategy
{
private boolean inReadFromSource= false;
@Override
public void readFromSource()
{
boolean oldInReadFromSource= inReadFromSource;
try
{
inReadFromSource= true;
super.readFromSource();
}
finally
{
inReadFromSource= oldInReadFromSource;
}
}
@Override
protected void afterMutate()
{
// only write changes to the model back if we're not
// in the process of reading it out.
if (!inReadFromSource)
{
writeToSource(false);
}
}
@Override
protected boolean computeDirty()
{
// we're never dirty
return false;
}
}
}