/*
Copyright (c) 2003-2009 ITerative Consulting Pty Ltd. All Rights Reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
o Redistributions of source code must retain the above copyright notice, this list of conditions and
the following disclaimer.
o Redistributions in binary form must reproduce the above copyright notice, this list of conditions
and the following disclaimer in the documentation and/or other materials provided with the distribution.
o This jcTOOL Helper Class software, whether in binary or source form may not be used within,
or to derive, any other product without the specific prior written permission of the copyright holder
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package DisplayProject.binding;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.util.HashMap;
import java.util.Map;
import javax.swing.SwingUtilities;
import org.apache.log4j.Logger;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import DisplayProject.actions.ActionMgr;
import DisplayProject.actions.PendingAction;
import DisplayProject.binding.value.AbstractValueModel;
import DisplayProject.binding.value.ValueModel;
import Framework.DataValue;
/**
* A factory used to retrieve value models for bean properties so they can
* be bound to swing components using different jgoodies adapter class. Bean
* property meta data in the form of property descriptors can also be obtained
* which is can be used at the time a binding is established. This can be useful
* when one of the jgoodies adapters is unable to support a particular binding
* scenario i.e binding text fields to textdatas.
* <p>
* A jgoodies presentation model arranged in a parent->child hierarchy is
* used internally to create value models for nested bean properties.
* PropertyDescriptors are supported by internally holding a spring bean wrapper
* for each object in the hierarchy.
*/
public class BindingSource
{
private static Logger logger = Logger.getLogger(BindingSource.class);
private static final String NESTED_PROPERTY_SEPERATOR = ".";
private BeanWrapper beanWrapper;
private PresentationModel presentationModel;
private Map<String, BindingSource> children = new HashMap<String, BindingSource>();
/**
* Creates a new bindingSource with the specified bean instance and initialises the
* underlying bean wrapper used to obtain property descriptors
* @param bean the java bean our presentation bindingSource is based on
*/
public BindingSource(Object bean)
{
this.presentationModel = new PresentationModel(bean);
this.beanWrapper = new BeanWrapperImpl(bean);
}
public void setBean(Object bean) {
this.beanWrapper.setWrappedInstance(bean);
this.presentationModel.setBean(bean);
}
public Object getBean() {
return this.presentationModel.getBean();
}
/**
* Creates a new child bindingSource from a previously obtained value bindingSource that will
* be used as the presentations models bean channel. Typically this value bindingSource
* will represent an adapted child bean that needs to be observed for changes
*
* @param beanChannel
* @param modelDescriptor
*/
protected BindingSource(ValueModel beanChannel, PropertyDescriptor modelDescriptor)
{
this.presentationModel = new PresentationModel(beanChannel);
this.beanWrapper = new BeanWrapperImpl(modelDescriptor.getPropertyType());
}
/**
* Adapts a value model to the specified property path. Properties are specified
* using strandard java bean naming conventions with a dot (.) used for nested
* properties
*
* @param propertyPath the property path to the value bindingSource
* @return A value model for the property path
*/
public ValueModel getValueModel(String propertyPath)
{
logger.debug("getValueModel(" + propertyPath + ")");
if (!isNestedProperty(propertyPath))
{
logger.debug("Not a nested property. Querying presentation bindingSource");
AbstractValueModel valueModel = presentationModel.getModel(propertyPath);
PropertyDescriptor descriptor = getPropertyDescriptor(propertyPath);
if (DataValue.class.isAssignableFrom(descriptor.getPropertyType()))
{
//DataValue instances contain a mutable setValue accessor that we need to also observe
//so we wrap the value model for thee adapted bean property with a special model
logger.debug("Wrapping value model with DataValue aware model");
valueModel = new DataValueValueModel(valueModel);
}
// Make sure the value models only ever send change events on the EDT
return new EDTSafeValueModelProxy(valueModel, descriptor);
}
logger.debug("Querying child with property path [" + getPropertyPathForChildModel(propertyPath) + "] for value bindingSource");
return getModelForNestedProperty(propertyPath).getValueModel(getPropertyPathForChildModel(propertyPath));
}
/**
* Retrieves a property descriptor for the given property path. Useful if meta data about
* a property is required
*
* @param propertyPath rep[resents the property we want meta data for
* @return a property descriptor for the property path
*/
public PropertyDescriptor getPropertyDescriptor(String propertyPath)
{
logger.debug("getPropertyDescriptor(" + propertyPath + ")");
if (!isNestedProperty(propertyPath))
{
logger.debug("Not a nested property. Querying bean wrapper directly");
return beanWrapper.getPropertyDescriptor(propertyPath);
}
logger.debug("Querying child with property path [" + getPropertyPathForChildModel(propertyPath) + "] for descriptor");
return getModelForNestedProperty(propertyPath).getPropertyDescriptor(getPropertyPathForChildModel(propertyPath));
}
private BindingSource getModelForNestedProperty(String propertyPath)
{
// Get the child bindingSource from the property path creating a new bindingSource if neccasary
logger.debug("Getting bindingSource for nested property [" + propertyPath + "]");
String childPropertyName = getChildPropertyName(propertyPath);
logger.debug("Child Property [" + childPropertyName + "]");
BindingSource childModel = children.get(childPropertyName);
if (childModel == null)
{
logger.debug("Creating new wrapper for child property [" + childPropertyName + "]");
childModel = new BindingSource(presentationModel.getModel(childPropertyName),getPropertyDescriptor(childPropertyName));
children.put(childPropertyName,childModel);
}
return childModel;
}
private boolean isNestedProperty(String propertyPath)
{
return (propertyPath.indexOf(NESTED_PROPERTY_SEPERATOR) != -1);
}
private String getChildPropertyName(String propertyPath)
{
return propertyPath.substring(0,propertyPath.indexOf(NESTED_PROPERTY_SEPERATOR));
}
private String getPropertyPathForChildModel(String propertyPath)
{
return propertyPath.substring(propertyPath.indexOf(NESTED_PROPERTY_SEPERATOR)+1);
}
/**
* A value model proxy that plays nice with swing and ensures that
* change events get reported on the EDT. This allows the model to
* be updated on a different thread i.e EventManager thread
*/
@SuppressWarnings("serial")
public class EDTSafeValueModelProxy extends AbstractValueModel implements PropertyChangeListener, TypeAwareValueModel
{
private AbstractValueModel targetValueModel;
private PropertyDescriptor valueDescriptor;
/**
* Constructs a new proxy with the given target model
* and meta data that describes the underlying value.
*
* @param target the valuemodel being proxied
* @param valueDescriptor a property descriptor for the underlying value
*/
public EDTSafeValueModelProxy(AbstractValueModel target, PropertyDescriptor valueDescriptor)
{
this.targetValueModel = target;
this.valueDescriptor = valueDescriptor;
this.targetValueModel.addValueChangeListener(this);
}
/**
* @see ValueModel
*/
public Object getValue()
{
logger.debug("getValue()");
return targetValueModel.getValue();
}
/**
* @see ValueModel
*/
public void setValue(Object newValue)
{
logger.debug("setValue(" + newValue + ")");
targetValueModel.setValue(performBasicConversion(newValue));
}
/**
* @see TypeAwareValueModel
*/
public Class<?> getValueType()
{
return valueDescriptor.getPropertyType();
}
/**
* Refire the event, but make sure it gets fired on the EDT
* @param evt
*/
public void propertyChange(final PropertyChangeEvent evt)
{
if (SwingUtilities.isEventDispatchThread())
{
logger.debug("Firing value change on EDT");
fireValueChange(evt.getOldValue(), evt.getNewValue());
}
else
{
ActionMgr.addAction(new PendingAction(null) {
public void performAction()
{
logger.debug("Firing value change inside performAction");
// We always fire a change unless the references are the same.
// The default property change support, will test if the old
// and new values are different using .equals()
fireValueChange(evt.getOldValue(), evt.getNewValue(), true);
}
@Override
public String toString() {
return "EDTSafeSet(Property:" + evt.getPropertyName() + " on " +
evt.getSource() + " from:" + evt.getOldValue() + " to:" + evt.getNewValue() +")";
}
});
}
}
/**
* Performs some simple conversions if the value being set is
* not compatible with the adapted bean property
* @param value
* @return
*/
private Object performBasicConversion(Object value) {
// TF:26/9/07:Refactored into seperate method
return TypeConverter.convert(value, getValueType());
}
}
}