/*
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.PropertyChangeSupport;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollBar;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import org.apache.log4j.Logger;
import org.springframework.beans.BeanUtils;
import DisplayProject.ArrayFieldModel;
import DisplayProject.DropListModel;
import DisplayProject.PaletteList;
import DisplayProject.RadioList;
import DisplayProject.RadioListModel;
import DisplayProject.ScrollListModel;
import DisplayProject.actions.ScrollBar;
import DisplayProject.binding.adapter.BoundedRangeAdapter;
import DisplayProject.binding.adapter.SingleListSelectionAdapter;
import DisplayProject.binding.list.SelectionInList;
import DisplayProject.binding.value.ValueHolder;
import DisplayProject.binding.value.ValueModel;
import DisplayProject.controls.AutoResizingComboBox;
import DisplayProject.controls.FillInField;
import DisplayProject.controls.MenuList;
import Framework.Array_Of_ListElement;
import Framework.ListElement;
import Framework.UsageException;
/**
* Manages aspects of data binding for a window. Ensures that the controls
* on a window stay in sync with the underlying data model and vice versa.
* <p>
* A BindingManager is typically initialised with java bean that contains one
* more public properties which are then bound to certain UI swing control using
* one of the provided "binding" methods. Properties are specified using standard
* java beans naming conventions. Support for nested properties of any depth is
* provided.
* <p>
* <strong>Example:</strong>
* <pre>
* // The java bean containing the bound properties
* Person person = new Person();
* // Create new BindingManager with specified java bean
* BindingManager bindingManager = new BindingManager(person);
*
* // Bind JTextField to "name" property
* JTextField nameField = new JTextField();
* bindingManager.bindComponent(nameField, "name" );
*
* // Bind JTextField to nested account number property
* JTextField accountNumberField = new JTextField();
* bindingManager.bindComponent(accountNumberField, "account.accountNumber" );
* </pre><p>
* The binding is implemented using JGoodies binding framework, that is based on common
* patterns such as ValueModel and PresentationModel.
* <p>
*/
public class BindingManager
{
//private static Logger logger = Logger.getLogger(BindingManager.class);
//private static final String NESTED_PROPERTY_SEPERATOR = ".";
private ComponentBinderFactory componentBinderFactory = new DefaultComponentBinderFactory();
private BindingSource model;
/**
* The path to which this widget is mapped. This is used to be able to resolve which object a data value is mapped to
*/
private static final String MAPPED_PATH_PROPERTY = "qq_MappedPath";
/**
* The client property that stores a reference to the binding manager for a component. CraigM:19/01/2009.
*/
public static final String BINDING_MANAGER_PROPERTY = "qq_BindingManagerReference";
/**
* Creates a new binding manager for the specified mapped
* object
* @param mappedObject
*/
public BindingManager(Object mappedObject) {
model = new BindingSource(mappedObject);
}
/**
* Create a new binding manager based on the passed mapped object
*/
public BindingManager(final Class<?> mappedObject) {
model = new BindingSource(BeanUtils.instantiateClass(mappedObject));
}
/**
* Resets the mapped object to a new one.
* @param mappedObject the new mapped object
*/
public void setMappedObject(Object mappedObject)
{
if (mappedObject != null) {
if (!mappedObject.getClass().equals(model.getBean().getClass())) {
throw new UsageException("Mapped object is the incorrect type");
}
model.setBean(mappedObject);
}
}
/**
* Retrive the object mapped to this binding manager
* @return the bound object
*/
public Object getMappedObject() {
return model.getBean();
}
/**
* Binds the specified control to the bean property represented by the
* specified property path. The type of component will determine how
* the binding is performed
*
* @param component the component we are binding
* @param propertyPath the full path of the property to bind
*/
public void bindComponent(JComponent component, String propertyPath)
{
// TF: 14/3/08:There are some components that aren't bound via the componentBinderFactory,
// such as grid fields, because all we use them for is to remember their property paths.
component.putClientProperty(MAPPED_PATH_PROPERTY, propertyPath);
// CraigM:19/01/2009 - Add a reference to the binding manager on the component so we can
// access the binding manager before the component is added to the window.
component.putClientProperty(BINDING_MANAGER_PROPERTY, this);
Binder binder = componentBinderFactory.createBinder(component);
if (binder != null) {
binder.bind(component,getPropertyValueModel(propertyPath));
}
}
/**
* Binds the scroll bar to the given object, with the passed min value, max value and extent
* @param scrollBar the scroll bar we are binding
* @param propertyPath the property to bind to the scroll bar value
* @param pMin the minimum value of the scrollbar
* @param pMax the maximum value of the scrollbar
* @param pExtent the extent of the scrollbar
*/
public void bindComponent(JScrollBar scrollBar, String propertyPath, int pMin, int pMax, int pExtent) {
scrollBar.putClientProperty(MAPPED_PATH_PROPERTY, propertyPath);
PropertyDescriptor descriptor = model.getPropertyDescriptor(propertyPath);
IntegerValueModelConverter adapter = new IntegerValueModelConverter(getPropertyValueModel(propertyPath), descriptor.getPropertyType());
// Prevent an exception being thrown if underlying value is out of the allowed range
Integer val = (Integer)adapter.getValue();
if (val == null || val.intValue() < pMin || val.intValue() > pMax) {
adapter.setValue(pMin);
}
// TF:15/6/10:Changed this to set the block increment on the actual control, and set the view size to
// the correct size once the model has been created.
scrollBar.setBlockIncrement(pExtent);
scrollBar.setModel(new BoundedRangeAdapter(adapter, pExtent, pMin, pMax));
ScrollBar.setViewSizeImmediate(scrollBar, pExtent);
}
/**
* Binds the combox boxes selected item and data source to the specified bean
* properties.
*
* @param comboBox the combox box we are binding
* @param selectedItemProperty the property to bind to the combo boxes selected item
* @param dataSourceProperty the property that will supply the combo box with its list of items
*/
// public void bindComponent(JComboBox comboBox, String selectedItemProperty, String dataSourceProperty)
// {
// SelectionInList selectionInList = new SelectionInList(getPropertyValueModel(dataSourceProperty),getPropertyValueModel(selectedItemProperty));
// Bindings.bind(comboBox,selectionInList);
// }
/**
* Binds the combox boxes selected item and constructed array of list elements
*
* AD:26/6/2008 Change JComboBox to AutoResizingComboBox to reduce casting later
* @param comboBox the combox box we are binding
* @param selectedItemProperty the property to bind to the combo boxes selected item
* @param elements an array of list elements that will be the combo boxes data source
* @param typeToMap should be any of integer.TYPE, IntegerData.class, String.Class or TextData.Class to describe where to put the informaiton
*/
@SuppressWarnings("unchecked")
public void bindComponent(AutoResizingComboBox comboBox, String selectedItemProperty, Array_Of_ListElement<ListElement> elements)
{
comboBox.putClientProperty(MAPPED_PATH_PROPERTY, selectedItemProperty);
// Clone the list so it doesn't reflect changes made by the user
Array_Of_ListElement<ListElement> newList = (Array_Of_ListElement)elements.clone();
// Unfortunately, we need to tie the list selection and the model together, so that we can
// get and set the element list from the model
ListElementValueModelConverter elementConverter = this.getElementConverter(selectedItemProperty, newList);
// CraigM:10/12/2008 - Flag we are a FillInField if necessary
if (comboBox instanceof FillInField) {
elementConverter.setIsFillInField();
}
else {
// TF:18/07/2009:We need to say that an element in the list is mandatory, so if we map an item which is not in the
// list of a drop list it will force it to be mapped. Note that in Forte, most of the drop lists do NOT have default
// values, and hence do NOT require this line. For example, a String mapped to a combo box outside of the scope of
// an object, or an object mapped to "A.B" as it's explicit property path would not set a default value to an item
// in the combo box. However, a combobox mapped to "B" which is a child of "A" will have a default value initailased.
// This latter situation is probably the more important situation, so we default the behaviour to this.
elementConverter.setForceValidValue(true);
}
//SelectionInList selection = new SelectionInList(newList,elementConverter);
comboBox.setModel(new DropListModel(newList, elementConverter, comboBox));
}
/**
* Binds the combox boxes selected item and constructed array of list elements
*
* @param comboBox the combox box we are binding
* @param selectedItemProperty the property to bind to the combo boxes selected item
* @param elements an array of object that will be the combo boxes data source
*/
// public void bindComponent(JComboBox comboBox, String selectedItemProperty, Object[] elements)
// {
// SelectionInList selectionInList = new SelectionInList(elements,getPropertyValueModel(selectedItemProperty));
// Bindings.bind(comboBox,selectionInList);
// }
/**
* Binds the lists selected item and constructed array of list elements
*
* @param list the list we are binding
* @param selectedItemProperty the property to bind to the lists selected item
* @param elements an array of list elements that will be the lists data source
* @param typeToMap should be any of integer.TYPE, IntegerData.class, String.Class or TextData.Class to describe where to put the informaiton
*/
public void bindComponent(JList list, String selectedItemProperty, Array_Of_ListElement<ListElement> elements) {
this.bindComponent(list, selectedItemProperty, elements, list.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION);
}
@SuppressWarnings("unchecked")
public void bindComponent(JList list, String selectedItemProperty, Array_Of_ListElement<ListElement> elements, boolean pIsSingleSelect)
{
list.putClientProperty(MAPPED_PATH_PROPERTY, selectedItemProperty);
// Clone the list so it doesn't reflect changes made by the user
Array_Of_ListElement<ListElement> newList = (Array_Of_ListElement<ListElement>)elements.clone();
// Unfortunately, we need to tie the list selection and the model together, so that we can
// get and set the element list from the model
// ListElementValueModelConverter elementConverter = this.getElementConverter(selectedItemProperty, newList);
//SelectionInList selection = new SelectionInList(newList,elementConverter);
SelectionInList selection = this.getSelectionInList(selectedItemProperty, newList);
list.setModel(new ScrollListModel(selection, list, pIsSingleSelect));
if (pIsSingleSelect) {
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setSelectionModel( new SingleListSelectionAdapter(
selection.getSelectionIndexHolder()));
// Force a selection if we're in single selection mode...
if (list.getSelectedIndex() == -1) {
list.setSelectedIndex(0);
}
}
else {
list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
list.setSelectionModel(new ToggleListSelectionModel());
list.addListSelectionListener(new SelectionCounterAdapter(list, selection.getSelectionIndexHolder()));
}
//Bindings.bind(list,getSelectionInList(selectedItemProperty,elements));
}
/**
* Binds a MenuList
* @param list
* @param selectedItemProperty
* @param elements
*/
@SuppressWarnings("unchecked")
public void bindComponent(MenuList list, String selectedItemProperty, Array_Of_ListElement<ListElement> elements)
{
Array_Of_ListElement<ListElement> newList = (Array_Of_ListElement<ListElement>)elements.clone();
// TF:08/11/2009:We need to be able to force the list to have a valid value (unless it's a null type)
SelectionInList selection = this.getSelectionInList(selectedItemProperty, newList, true);
list.setModel(new MenuList.Model(selection, list));
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setSelectionModel( new SingleListSelectionAdapter(
selection.getSelectionIndexHolder()));
// Force a selection if we're in single selection mode...
// TF:08/11/2009:DET-127:We only want to do this on non-nullable types, so invoke a method to do it.
list.initialiseSelectedElement();
}
/**
* Binds the lists selected item and constructed array of list elements
*
* @param list the list we are binding
* @param selectedItemProperty the property to bind to the lists selected item
* @param elements an array of list elements that will be the lists data source
* @param typeToMap should be any of integer.TYPE, IntegerData.class, String.Class or TextData.Class to describe where to put the informaiton
*/
@SuppressWarnings("unchecked")
public void bindComponent(final RadioList list, String selectedItemProperty, Array_Of_ListElement<ListElement> elements)
{
list.putClientProperty(MAPPED_PATH_PROPERTY, selectedItemProperty);
// Clone the list so it doesn't reflect changes made by the user
Array_Of_ListElement<ListElement> newList = (Array_Of_ListElement<ListElement>)elements.clone();
// Unfortunately, we need to tie the list selection and the model together, so that we can
// get and set the element list from the model
// ListElementValueModelConverter elementConverter = this.getElementConverter(selectedItemProperty, newList);
SelectionInList selection = this.getSelectionInList(selectedItemProperty, newList);
list.setModel(new RadioListModel(selection, list));
list.setSelectionModel( new SingleListSelectionAdapter(
selection.getSelectionIndexHolder()));
//Bindings.bind(list,getSelectionInList(selectedItemProperty,elements));
}
/**
* Adds a binding between the specified table and row source property.
* The row bean type and bean property names are used to bind bean properties
* to columns
*
* @param table the table we are binding
* @param tableRowSourcePropertyPath the property path corresponding to the tables row source
* @param rowBeanType the type of java bean that each row is bound to
* @param beanPropertyNames the bean property names we will bind to each column
*/
public void bindComponent(JTable table, String tableRowSourcePropertyPath, Class<?> rowBeanType, String[] beanPropertyNames, JFrame root)
{
table.putClientProperty(MAPPED_PATH_PROPERTY, tableRowSourcePropertyPath);
this.bindComponent(table, tableRowSourcePropertyPath, rowBeanType, beanPropertyNames);
try {
Field f = root.getClass().getField("qq_Listeners");
if (f != null)
{
PropertyChangeSupport pcs = (PropertyChangeSupport)f.get(root);
pcs.addPropertyChangeListener(tableRowSourcePropertyPath, (ArrayFieldModel)table.getModel());
}
} catch (SecurityException e) {
Logger.getLogger(getClass()).error(e);
} catch (IllegalArgumentException e) {
Logger.getLogger(getClass()).error(e);
} catch (NoSuchFieldException e) {
Logger.getLogger(getClass()).error(e);
} catch (IllegalAccessException e) {
Logger.getLogger(getClass()).error(e);
}
}
public void bindComponent(JTable table, String tableRowSourcePropertyPath, Class<?> rowBeanType, String[] beanPropertyNames)
{
table.putClientProperty(MAPPED_PATH_PROPERTY, tableRowSourcePropertyPath);
ObjectTableModel tableModel = new ObjectTableModel(table,rowBeanType,beanPropertyNames);
table.setModel(tableModel);
tableModel.setRowSource(getPropertyValueModel(tableRowSourcePropertyPath));
}
/**
* Gets the value model for the given nested property path.
*
* @param propertyPath the java bean property path
* @return a ValueModel for the given property
*/
private ValueModel getPropertyValueModel(String propertyPath)
{
// TF:07/10/2009:If we have a debug setting on in the log4j.properties file, we will
// validate that the passed property path will resolve to a valid field.
return model.getValueModel(propertyPath);
}
private ListElementValueModelConverter getElementConverter(String selectedItemProperty, List<ListElement> elements) {
ValueModel selectedItem = getPropertyValueModel(selectedItemProperty);
PropertyDescriptor descriptor = model.getPropertyDescriptor(selectedItemProperty);
ValueHolder aHolder = new ValueHolder(elements, false);
return new ListElementValueModelConverter(selectedItem, aHolder, descriptor.getPropertyType());
}
private SelectionInList getSelectionInList(String selectedItemProperty, List<ListElement> elements, boolean pForceValidValue)
{
ValueModel selectedItem = getPropertyValueModel(selectedItemProperty);
PropertyDescriptor descriptor = model.getPropertyDescriptor(selectedItemProperty);
ValueHolder aHolder = new ValueHolder(elements, false);
ListElementValueModelConverter elementConverter = new ListElementValueModelConverter(selectedItem, aHolder, descriptor.getPropertyType());
elementConverter.setForceValidValue(pForceValidValue);
SelectionInList selectionInList = new SelectionInList(aHolder, elementConverter);
return selectionInList;
}
private SelectionInList getSelectionInList(String selectedItemProperty, List<ListElement> elements) {
return getSelectionInList(selectedItemProperty, elements, false);
}
@SuppressWarnings("unchecked")
public void bindComponent(PaletteList list, String selectedItemProperty, Array_Of_ListElement<ListElement> elements) {
// Clone the list so it doesn't reflect changes made by the user
Array_Of_ListElement<ListElement> newList = (Array_Of_ListElement<ListElement>)elements.clone();
// Unfortunately, we need to tie the list selection and the model together, so that we can
// get and set the element list from the model
SelectionInList selection = this.getSelectionInList(selectedItemProperty, newList);
list.setElementList(newList);
list.setSelectionModel( new SingleListSelectionAdapter(selection.getSelectionIndexHolder()));
if (list.getSelectedIndex() == -1) {
list.setSelectedIndex(0);
}
}
/**
* Return the object currently bound to the specified property path. If the property path is unknown,
* null is returned.
* @param propertyPath
* @return
*/
public Object getDataObject(String propertyPath) {
if (propertyPath == null || this.model == null) {
return null;
}
ValueModel model = this.model.getValueModel(propertyPath);
if (model == null) {
return null;
}
return model.getValue();
}
/**
* Return the object currently bound to the specified control from this binding manager. If the control has not
* been bound with this binding manager, null will be returned
* @param propertyPath
* @return
*/
public Object getDataObject(JComponent component) {
return getDataObject((String)component.getClientProperty(MAPPED_PATH_PROPERTY));
}
/**
* Get the path to which this object is bound, in the format of a.b.c
* @param component
* @return
*/
public static String getDataObjectPath(JComponent component) {
return (String)component.getClientProperty(MAPPED_PATH_PROPERTY);
}
/**
* A wrapper around a presentation model to support nested property paths
* and property meta-data in the form of property descriptors.
* <p>
* To support nested properties this class is organised into a parent child
* hierarchy. Metadata is obtained from a Spring bean wrapper
*/
// private class PresentationModelWrapper
// {
// private BeanWrapper beanWrapper;
// private PresentationModel presentationModel;
// private Map children = new HashMap();
//
// /**
// * Creates a new model with the specified bean instance and initialises the
// * underlying bean wrapper used to obtain property descriptors
// * @param bean the java bean our presentation model is based on
// */
// public PresentationModelWrapper(Object bean)
// {
// this.presentationModel = new PresentationModel(bean);
// this.beanWrapper = new BeanWrapperImpl(bean);
// }
//
// /**
// * Creates a new child model from a previously obtained value model that will
// * be used as the presentations models bean channel. Typically this value model
// * will represent an adapted child bean that needs to be observed for changes
// *
// * @param beanChannel
// * @param modelDescriptor
// */
// protected PresentationModelWrapper(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 model
// * @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 model");
// return presentationModel.getModel(propertyPath);
// }
//
// logger.debug("Querying child with property path [" + getPropertyPathForChildModel(propertyPath) + "] for value model");
// 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 PresentationModelWrapper getModelForNestedProperty(String propertyPath)
// {
// // Get the child model from the property path creating a new model if neccasary
// logger.debug("Getting model for nested property [" + propertyPath + "]");
// String childPropertyName = getChildPropertyName(propertyPath);
// logger.debug("Child Property [" + childPropertyName + "]");
// PresentationModelWrapper childModel = (PresentationModelWrapper) children.get(childPropertyName);
// if (childModel == null)
// {
// logger.debug("Creating new wrapper for child property [" + childPropertyName + "]");
// childModel = new PresentationModelWrapper(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);
// }
// }
}