/**
* 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.form.support;
import org.springframework.beans.BeanUtils;
import org.springframework.binding.collection.AbstractCachingMapDecorator;
import org.springframework.binding.convert.ConversionExecutor;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.convert.converters.Converter;
import org.springframework.binding.convert.service.DefaultConversionService;
import org.springframework.binding.convert.service.GenericConversionService;
import org.springframework.util.Assert;
import org.valkyriercp.binding.MutablePropertyAccessStrategy;
import org.valkyriercp.binding.PropertyAccessStrategy;
import org.valkyriercp.binding.PropertyMetadataAccessStrategy;
import org.valkyriercp.binding.form.*;
import org.valkyriercp.binding.support.BeanPropertyAccessStrategy;
import org.valkyriercp.binding.value.CommitTrigger;
import org.valkyriercp.binding.value.DirtyTrackingValueModel;
import org.valkyriercp.binding.value.ValueModel;
import org.valkyriercp.binding.value.support.BufferedValueModel;
import org.valkyriercp.binding.value.support.MethodInvokingDerivedValueModel;
import org.valkyriercp.binding.value.support.TypeConverter;
import org.valkyriercp.binding.value.support.ValueHolder;
import org.valkyriercp.core.support.AbstractPropertyChangePublisher;
import org.valkyriercp.util.ClassUtils;
import org.valkyriercp.util.EventListenerListHelper;
import org.valkyriercp.util.ValkyrieRepository;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
/**
* Base implementation of HierarchicalFormModel and ConfigurableFormModel
* subclasses need only implement the 4 value model interception methods.
*
* @author Keith Donald
* @author Oliver Hutchison
*/
public abstract class AbstractFormModel extends AbstractPropertyChangePublisher implements HierarchicalFormModel,
ConfigurableFormModel {
private String id;
private final FormModelMediatingValueModel formObjectHolder;
private final MutablePropertyAccessStrategy propertyAccessStrategy;
private HierarchicalFormModel parent;
private final List children = new ArrayList();
private boolean buffered = false;
private boolean enabled = true;
private boolean oldEnabled = true;
private boolean readOnly = false;
private boolean oldReadOnly = false;
private boolean authorized = true;
private boolean oldDirty;
private boolean oldCommittable = true;
private final CommitTrigger commitTrigger = new CommitTrigger();
private final Map mediatingValueModels = new HashMap();
private final Map propertyValueModels = new HashMap();
private final Map convertingValueModels = new HashMap();
private final Map fieldMetadata = new HashMap();
private final Set dirtyValueAndFormModels = new HashSet();
private final Map propertyConversionServices = new AbstractCachingMapDecorator() {
public Object create(Object key) {
return new DefaultConversionService() {
protected void addDefaultConverters() {
}
};
}
};
protected final PropertyChangeListener parentStateChangeHandler = new ParentStateChangeHandler();
protected final PropertyChangeListener childStateChangeHandler = new ChildStateChangeHandler();
private final EventListenerListHelper commitListeners = new EventListenerListHelper(CommitListener.class);
private Class defaultInstanceClass;
private ConversionService conversionService;
protected AbstractFormModel() {
this(new ValueHolder());
}
protected AbstractFormModel(Object domainObject) {
this(new ValueHolder(domainObject), true);
}
public AbstractFormModel(Object domainObject, boolean buffered) {
this(new ValueHolder(domainObject), buffered);
}
protected AbstractFormModel(ValueModel domainObjectHolder, boolean buffered) {
this(new BeanPropertyAccessStrategy(domainObjectHolder), buffered);
}
protected AbstractFormModel(MutablePropertyAccessStrategy propertyAccessStrategy, boolean buffered) {
ValueModel domainObjectHolder = propertyAccessStrategy.getDomainObjectHolder();
prepareValueModel(domainObjectHolder);
this.formObjectHolder = new FormModelMediatingValueModel(domainObjectHolder, false);
this.propertyAccessStrategy = propertyAccessStrategy;
this.buffered = buffered;
if (domainObjectHolder.getValue() != null)
this.defaultInstanceClass = domainObjectHolder.getValue().getClass();
}
/**
* Prepare the provided value model for use in this form model.
* @param valueModel to prepare
*/
protected void prepareValueModel(ValueModel valueModel) {
if (valueModel instanceof BufferedValueModel) {
((BufferedValueModel) valueModel).setCommitTrigger(commitTrigger);
}
// If the value model that we were built on is "dirty trackable" then we
// need to monitor it for changes in its dirty state
if (valueModel instanceof DirtyTrackingValueModel) {
((DirtyTrackingValueModel) valueModel).addPropertyChangeListener(DIRTY_PROPERTY, childStateChangeHandler);
}
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Object getFormObject() {
return getFormObjectHolder().getValue();
}
public void setFormObject(Object formObject) {
setDeliverValueChangeEvents(false);
if (formObject == null) {
handleSetNullFormObject();
}
else {
getFormObjectHolder().setValue(formObject);
setEnabled(true);
}
// this will cause all buffered value models to revert
// to the new form objects property values
commitTrigger.revert();
clearValueModelsDirtyState();
setDeliverValueChangeEvents(true);
}
/**
* Disconnect view from data in MediatingValueModels
*
* @param enable <code>true</code> if events should be
* delivered.
*/
private void setDeliverValueChangeEvents(boolean enable) {
formObjectHolder.setDeliverValueChangeEvents(enable);
for (Object o : mediatingValueModels.values()) {
FormModelMediatingValueModel valueModel = (FormModelMediatingValueModel) o;
valueModel.setDeliverValueChangeEvents(enable);
}
}
private void clearValueModelsDirtyState() {
for (Object o : mediatingValueModels.values()) {
((FormModelMediatingValueModel) o).clearDirty();
}
}
private void setDeliverValueChangeEvents(boolean deliverValueChangeEvents, boolean clearValueModels) {
formObjectHolder.setDeliverValueChangeEvents(deliverValueChangeEvents);
for (Object o : mediatingValueModels.values()) {
FormModelMediatingValueModel valueModel = (FormModelMediatingValueModel) o;
valueModel.setDeliverValueChangeEvents(deliverValueChangeEvents);
}
}
public void setDefaultInstanceClass(Class defaultInstanceClass) {
this.defaultInstanceClass = defaultInstanceClass;
}
public Class getDefaultInstanceClass() {
return defaultInstanceClass;
}
protected void handleSetNullFormObject() {
if (logger.isInfoEnabled()) {
logger.info("New form object value is null; resetting to a new fresh object instance and disabling form");
}
if (getDefaultInstanceClass() != null) {
getFormObjectHolder().setValue(BeanUtils.instantiateClass(getDefaultInstanceClass()));
}
else { // old behaviour
getFormObjectHolder().setValue(BeanUtils.instantiateClass(getFormObject().getClass()));
}
setEnabled(false);
}
/**
* Returns the value model which holds the object currently backing this
* form.
*/
public ValueModel getFormObjectHolder() {
return formObjectHolder;
}
public HierarchicalFormModel getParent() {
return parent;
}
/**
* {@inheritDoc}
*
* When the parent is set, the enabled and read-only states are bound and
* updated as needed.
*/
public void setParent(HierarchicalFormModel parent) {
Assert.notNull(parent, "parent");
this.parent = parent;
this.parent.addPropertyChangeListener(ENABLED_PROPERTY, parentStateChangeHandler);
this.parent.addPropertyChangeListener(READONLY_PROPERTY, parentStateChangeHandler);
enabledUpdated();
readOnlyUpdated();
}
public void removeParent() {
this.parent.removePropertyChangeListener(READONLY_PROPERTY, parentStateChangeHandler);
this.parent.removePropertyChangeListener(ENABLED_PROPERTY, parentStateChangeHandler);
this.parent = null;
readOnlyUpdated();
enabledUpdated();
}
public FormModel[] getChildren() {
return (FormModel[]) children.toArray(new FormModel[children.size()]);
}
/**
* Add child to this FormModel. Dirty and committable changes are forwarded
* to parent model.
* @param child FormModel to add as child.
*/
public void addChild(HierarchicalFormModel child) {
Assert.notNull(child, "child");
if (child.getParent() == this)
return;
Assert.isTrue(child.getParent() == null, "Child form model '" + child + "' already has a parent");
child.setParent(this);
children.add(child);
child.addPropertyChangeListener(DIRTY_PROPERTY, childStateChangeHandler);
child.addPropertyChangeListener(COMMITTABLE_PROPERTY, childStateChangeHandler);
if (child.isDirty())
{
dirtyValueAndFormModels.add(child);
dirtyUpdated();
}
}
/**
* Remove a child FormModel. Dirty and committable listeners are removed.
* When child was dirty, remove the formModel from the dirty list and update
* the dirty state.
* @param child FormModel to remove from childlist.
*/
public void removeChild(HierarchicalFormModel child) {
Assert.notNull(child, "child");
child.removeParent();
children.remove(child);
child.removePropertyChangeListener(DIRTY_PROPERTY, childStateChangeHandler);
child.removePropertyChangeListener(COMMITTABLE_PROPERTY, childStateChangeHandler);
// when dynamically adding/removing childModels take care of
// dirtymessages:
// removing child that was dirty: remove from dirty map and update dirty
// state
if (dirtyValueAndFormModels.remove(child))
dirtyUpdated();
}
public boolean hasValueModel(String formProperty) {
return propertyValueModels.containsKey(formProperty);
}
public ValueModel getValueModel(String formProperty) {
ValueModel propertyValueModel = (ValueModel) propertyValueModels.get(formProperty);
if (propertyValueModel == null) {
propertyValueModel = add(formProperty);
}
return propertyValueModel;
}
public ValueModel getValueModel(String formProperty, Class targetClass) {
final ConvertingValueModelKey key = new ConvertingValueModelKey(formProperty, targetClass);
ValueModel convertingValueModel = (ValueModel) convertingValueModels.get(key);
if (convertingValueModel == null) {
convertingValueModel = createConvertingValueModel(formProperty, targetClass);
convertingValueModels.put(key, convertingValueModel);
}
return convertingValueModel;
}
/**
* Creates a new value mode for the the given property. Usually delegates to
* the underlying property access strategy but subclasses may provide
* alternative value model creation strategies.
*/
protected ValueModel createValueModel(String formProperty) {
Assert.notNull(formProperty, "formProperty");
if (logger.isDebugEnabled()) {
logger.debug("Creating " + (buffered ? "buffered" : "") + " value model for form property '" + formProperty
+ "'.");
}
return buffered ? new BufferedValueModel(propertyAccessStrategy.getPropertyValueModel(formProperty))
: propertyAccessStrategy.getPropertyValueModel(formProperty);
}
protected ValueModel createConvertingValueModel(String formProperty, Class targetClass) {
if (logger.isDebugEnabled()) {
logger.debug("Creating converting value model for form property '" + formProperty
+ "' converting to type '" + targetClass + "'.");
}
final ValueModel sourceValueModel = getValueModel(formProperty);
Assert.notNull(sourceValueModel, "Form does not have a property called '" + formProperty + "'.");
final Class sourceClass = ClassUtils
.convertPrimitiveToWrapper(getFieldMetadata(formProperty).getPropertyType());
// sour.required(ceClass can be null when using eg Map, assume that given
// targetClass is the correct one
if ((sourceClass == null) || (sourceClass == targetClass)) {
return sourceValueModel;
}
final ConversionService conversionService = getConversionService();
ConversionExecutor convertTo = null;
ConversionExecutor convertFrom = null;
// Check for locally registered property converters
if (propertyConversionServices.containsKey(formProperty)) {
// TODO - extract ConfigurableConversionService interface...
final GenericConversionService propertyConversionService = (GenericConversionService) propertyConversionServices
.get(formProperty);
if (propertyConversionService != null) {
convertTo = propertyConversionService.getConversionExecutor(sourceClass, targetClass);
convertFrom = propertyConversionService.getConversionExecutor(targetClass, sourceClass);
}
}
// If we have nothing from the property level, then try the conversion
// service
if (convertTo == null) {
convertTo = conversionService.getConversionExecutor(sourceClass, targetClass);
}
Assert.notNull(convertTo, "conversionService returned null ConversionExecutor");
if (convertFrom == null) {
convertFrom = conversionService.getConversionExecutor(targetClass, sourceClass);
}
Assert.notNull(convertFrom, "conversionService returned null ConversionExecutor");
ValueModel convertingValueModel = preProcessNewConvertingValueModel(formProperty, targetClass,
new TypeConverter(sourceValueModel, convertTo, convertFrom));
preProcessNewConvertingValueModel(formProperty, targetClass, convertingValueModel);
return convertingValueModel;
}
/**
* Register converters for a given property name.
* @param propertyName name of property on which to register converters
* @param toConverter Convert from source to target type
* @param fromConverter Convert from target to source type
*/
public void registerPropertyConverter(String propertyName, Converter toConverter, Converter fromConverter) {
DefaultConversionService propertyConversionService = (DefaultConversionService) propertyConversionServices
.get(propertyName);
propertyConversionService.addConverter(toConverter);
propertyConversionService.addConverter(fromConverter);
}
public ValueModel add(String propertyName) {
return add(propertyName, createValueModel(propertyName));
}
public ValueModel add(String formProperty, ValueModel valueModel) {
// XXX: this assert should be active but it breaks the
// code in SwingBindingFactory#createBoundListModel
// Assert.isTrue(!hasValueModel(formProperty), "A property called '" +
// formProperty + "' already exists.");
if (valueModel instanceof BufferedValueModel) {
((BufferedValueModel) valueModel).setCommitTrigger(commitTrigger);
}
PropertyMetadataAccessStrategy metadataAccessStrategy = getFormObjectPropertyAccessStrategy()
.getMetadataAccessStrategy();
FormModelMediatingValueModel mediatingValueModel = new FormModelMediatingValueModel(valueModel,
metadataAccessStrategy.isWriteable(formProperty));
mediatingValueModels.put(formProperty, mediatingValueModel);
FieldMetadata metadata = new DefaultFieldMetadata(this, mediatingValueModel, metadataAccessStrategy
.getPropertyType(formProperty), !metadataAccessStrategy.isWriteable(formProperty),
metadataAccessStrategy.getAllUserMetadata(formProperty));
metadata.addPropertyChangeListener(FieldMetadata.DIRTY_PROPERTY, childStateChangeHandler);
return add(formProperty, mediatingValueModel, metadata);
}
/**
* {@inheritDoc}
*/
public ValueModel add(String propertyName, ValueModel valueModel, FieldMetadata metadata) {
fieldMetadata.put(propertyName, metadata);
valueModel = preProcessNewValueModel(propertyName, valueModel);
propertyValueModels.put(propertyName, valueModel);
if (logger.isDebugEnabled()) {
logger.debug("Registering '" + propertyName + "' form property, property value model=" + valueModel);
}
postProcessNewValueModel(propertyName, valueModel);
return valueModel;
}
/**
* Provides a hook for subclasses to optionally decorate a new value model
* added to this form model.
*/
protected abstract ValueModel preProcessNewValueModel(String formProperty, ValueModel formValueModel);
/**
* Provides a hook for subclasses to perform some processing after a new
* value model has been added to this form model.
*/
protected abstract void postProcessNewValueModel(String formProperty, ValueModel valueModel);
/**
* Provides a hook for subclasses to optionally decorate a new converting
* value model added to this form model.
*/
protected abstract ValueModel preProcessNewConvertingValueModel(String formProperty, Class targetClass,
ValueModel formValueModel);
/**
* Provides a hook for subclasses to perform some processing after a new
* converting value model has been added to this form model.
*/
protected abstract void postProcessNewConvertingValueModel(String formProperty, Class targetClass,
ValueModel valueModel);
public FieldMetadata getFieldMetadata(String propertyName) {
FieldMetadata metadata = (FieldMetadata) fieldMetadata.get(propertyName);
if (metadata == null) {
add(propertyName);
metadata = (FieldMetadata) fieldMetadata.get(propertyName);
}
return metadata;
}
/**
* {@inheritDoc}
*/
public Set getFieldNames() {
return Collections.unmodifiableSet(propertyValueModels.keySet());
}
protected FieldFaceSource getFieldFaceSource() {
return ValkyrieRepository.getInstance().getApplicationConfig().fieldFaceSource();
}
public FieldFace getFieldFace(String field) {
return getFieldFaceSource().getFieldFace(field, this);
}
public ValueModel addMethod(String propertyMethodName, String derivedFromProperty) {
return addMethod(propertyMethodName, new String[] { derivedFromProperty });
}
public ValueModel addMethod(String propertyMethodName, String[] derivedFromProperties) {
ValueModel[] propertyValueModels = new ValueModel[derivedFromProperties.length];
for (int i = 0; i < propertyValueModels.length; i++) {
propertyValueModels[i] = getValueModel(derivedFromProperties[i]);
}
ValueModel valueModel = new MethodInvokingDerivedValueModel(this, propertyMethodName, propertyValueModels);
return add(propertyMethodName, valueModel);
}
public ConversionService getConversionService() {
if(conversionService == null)
return ValkyrieRepository.getInstance().getApplicationConfig().conversionService();
return conversionService;
}
public MutablePropertyAccessStrategy getFormObjectPropertyAccessStrategy() {
return propertyAccessStrategy;
}
public PropertyAccessStrategy getPropertyAccessStrategy() {
return new FormModelPropertyAccessStrategy(this);
}
public void commit() {
if (logger.isDebugEnabled()) {
logger.debug("Commit requested for this form model " + this);
}
if (getFormObject() == null) {
if (logger.isDebugEnabled()) {
logger.debug("Form object is null; nothing to commit.");
}
return;
}
if (isCommittable()) {
for (Iterator i = commitListeners.iterator(); i.hasNext();) {
((CommitListener) i.next()).preCommit(this);
}
preCommit();
if (isCommittable()) {
doCommit();
postCommit();
for (Iterator i = commitListeners.iterator(); i.hasNext();) {
((CommitListener) i.next()).postCommit(this);
}
}
else {
throw new IllegalStateException("Form model '" + this
+ "' became non-committable after preCommit phase");
}
}
else {
throw new IllegalStateException("Form model '" + this + "' is not committable");
}
}
private void doCommit() {
for (Iterator i = children.iterator(); i.hasNext();) {
((FormModel) i.next()).commit();
}
commitTrigger.commit();
for (Iterator i = mediatingValueModels.values().iterator(); i.hasNext();) {
((DirtyTrackingValueModel) i.next()).clearDirty();
}
}
/**
* Hook for subclasses to intercept before a commit.
*/
protected void preCommit() {
}
/**
* Hook for subclasses to intercept after a successful commit has finished.
*/
protected void postCommit() {
}
/**
* Revert state. If formModel has children, these will be reverted first.
* CommitTrigger is used to revert bufferedValueModels while
* revertToOriginal() is called upon FormMediatingValueModels.
*/
public void revert() {
for (Iterator i = children.iterator(); i.hasNext();) {
((FormModel) i.next()).revert();
}
// this will cause all buffered value models to revert
commitTrigger.revert();
// this will then go back and revert all unbuffered value models
for (Iterator i = mediatingValueModels.values().iterator(); i.hasNext();) {
((DirtyTrackingValueModel) i.next()).revertToOriginal();
}
}
/**
* Complex forms with parent-child relations can use derived formModels.
* Such a Hierarchical tree cannot have its children reset on its own as it
* would break the top-down structure. see RCP-329 and the cvs maillist.
*
* TODO add a unit test with such a complex use case
*
* @see FormModel#reset()
*/
public void reset() {
setFormObject(null);
}
public boolean isBuffered() {
return buffered;
}
/**
* Returns <code>true</code> if this formModel or any of its children has
* dirty valueModels.
*/
public boolean isDirty() {
return dirtyValueAndFormModels.size() > 0;
}
/**
* Fires the necessary property change event for changes to the dirty
* property. Must be called whenever the value of dirty is changed.
*/
protected void dirtyUpdated() {
boolean dirty = isDirty();
if (hasChanged(oldDirty, dirty)) {
oldDirty = dirty;
firePropertyChange(DIRTY_PROPERTY, !dirty, dirty);
}
}
public void setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
readOnlyUpdated();
}
public boolean isReadOnly() {
return readOnly || !authorized || (parent != null && parent.isReadOnly());
}
/**
* Check if the form has the correct authorization and can be edited.
*
* @return <code>true</code> if this form is authorized and may be edited.
*/
public boolean isAuthorized() {
return authorized;
}
/**
* Set whether or not the form is authorized and can be edited.
*
* @param authorized <code>true</code> if this form may be edited.
*/
public void setAuthorized(boolean authorized) {
this.authorized = authorized;
readOnlyUpdated();
}
/**
* Fires the necessary property change event for changes to the readOnly
* property. Must be called whenever the value of readOnly is changed.
*/
protected void readOnlyUpdated() {
boolean localReadOnly = isReadOnly();
if (hasChanged(oldReadOnly, localReadOnly)) {
oldReadOnly = localReadOnly;
firePropertyChange(READONLY_PROPERTY, !localReadOnly, localReadOnly);
}
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
enabledUpdated();
}
public boolean isEnabled() {
return enabled && (parent == null || parent.isEnabled());
}
/**
* Fires the necessary property change event for changes to the enabled
* property. Must be called whenever the value of enabled is changed.
*/
protected void enabledUpdated() {
boolean enabled = isEnabled();
if (hasChanged(oldEnabled, enabled)) {
oldEnabled = enabled;
firePropertyChange(ENABLED_PROPERTY, !enabled, enabled);
}
}
public boolean isCommittable() {
for (Iterator i = children.iterator(); i.hasNext();) {
final FormModel childFormModel = (FormModel) i.next();
if (!childFormModel.isCommittable()) {
return false;
}
}
return true;
}
/**
* Fires the necessary property change event for changes to the committable
* property. Must be called whenever the value of committable is changed.
*/
protected void committableUpdated() {
boolean committable = isCommittable();
if (hasChanged(oldCommittable, committable)) {
oldCommittable = committable;
firePropertyChange(COMMITTABLE_PROPERTY, !committable, committable);
}
}
public void addCommitListener(CommitListener listener) {
commitListeners.add(listener);
}
public void removeCommitListener(CommitListener listener) {
commitListeners.remove(listener);
}
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Listener to be registered on properties of the parent form model. Calls
* are delegated to
* {@link AbstractFormModel#parentStateChanged(java.beans.PropertyChangeEvent)}. This
* way subclasses can extend the parent->child behaviour meaning state
* changes in the parent that influence the children.
*/
protected class ParentStateChangeHandler implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
parentStateChanged(evt);
}
}
/**
* Events from the parent form model that have side-effects on this form
* model should be handled here. This includes:
*
* <ul>
* <li><em>Enabled state:</em> when parent gets disabled, child should be
* disabled as well. If parent is enabled, child should go back to its
* original state.</li>
* <li><em>Read-only state:</em> when a parent is set read-only, child
* should be read-only as well. If parent is set editable, child should go
* back to its original state.</li>
* </ul>
*/
protected void parentStateChanged(PropertyChangeEvent evt) {
if (ENABLED_PROPERTY.equals(evt.getPropertyName())) {
enabledUpdated();
}
else if (READONLY_PROPERTY.equals(evt.getPropertyName())) {
readOnlyUpdated();
}
}
/**
* Listener to be registered on properties of child form models and other
* valueModels. Calls are delegated to
* {@link AbstractFormModel#childStateChanged(PropertyChangeEvent)}. This
* way subclasses can extend the child->parent behaviour meaning state
* changes in the child that influence the parent.
*/
protected class ChildStateChangeHandler implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
childStateChanged(evt);
}
}
/**
* Events from the child form model or value models that have side-effects
* on this form model should be handled here. This includes:
*
* <ul>
* <li><em>Dirty state:</em> when a child is dirty, the parent should be
* dirty.</li>
* <li><em>Committable state:</em> when a child is committable, the
* parent might be committable as well. The committable state of the parent
* should be taken into account and revised when a child sends this event.</li>
* </ul>
*
* Note that we include value models and their metadata as being children.
* As these are low level models, they cannot be parents and therefore don't
* show up in
* {@link AbstractFormModel#parentStateChanged(PropertyChangeEvent)}.
*/
protected void childStateChanged(PropertyChangeEvent evt) {
if (FormModel.DIRTY_PROPERTY.equals(evt.getPropertyName())) {
Object source = evt.getSource();
if (source instanceof FieldMetadata) {
FieldMetadata metadata = (FieldMetadata) source;
if (metadata.isDirty()) {
dirtyValueAndFormModels.add(metadata);
}
else {
dirtyValueAndFormModels.remove(metadata);
}
}
else if (source instanceof FormModel) {
FormModel formModel = (FormModel) source;
if (formModel.isDirty()) {
dirtyValueAndFormModels.add(formModel);
}
else {
dirtyValueAndFormModels.remove(formModel);
}
}
else {
DirtyTrackingValueModel valueModel = (DirtyTrackingValueModel) source;
if (valueModel.isDirty()) {
dirtyValueAndFormModels.add(valueModel);
}
else {
dirtyValueAndFormModels.remove(valueModel);
}
}
dirtyUpdated();
}
else if (COMMITTABLE_PROPERTY.equals(evt.getPropertyName())) {
committableUpdated();
}
}
/**
* Class for keys in the convertingValueModels map.
*/
protected static class ConvertingValueModelKey {
private final String propertyName;
private final Class targetClass;
public ConvertingValueModelKey(String propertyName, Class targetClass) {
this.propertyName = propertyName;
this.targetClass = targetClass;
}
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof ConvertingValueModelKey))
return false;
final ConvertingValueModelKey key = (ConvertingValueModelKey) o;
return propertyName.equals(key.propertyName) && (targetClass == key.targetClass);
}
public int hashCode() {
return (propertyName.hashCode() * 29) + (targetClass == null ? 7 : targetClass.hashCode());
}
}
}