com.jgoodies.binding.beans.PropertyConnector Maven / Gradle / Ivy
Show all versions of jgoodies-binding Show documentation
/*
* Copyright (c) 2002-2013 JGoodies Software GmbH. 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 Neither the name of JGoodies Software GmbH nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 com.jgoodies.binding.beans;
import static com.jgoodies.common.base.Preconditions.checkArgument;
import static com.jgoodies.common.base.Preconditions.checkNotBlank;
import static com.jgoodies.common.base.Preconditions.checkNotNull;
import static com.jgoodies.common.internal.Messages.MUST_NOT_BE_BLANK;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.beans.PropertyVetoException;
import com.jgoodies.binding.adapter.BasicComponentFactory;
import com.jgoodies.binding.adapter.Bindings;
import com.jgoodies.binding.value.ValueModel;
import com.jgoodies.common.base.Objects;
/**
* Keeps two Java Bean properties in synch. This connector supports bound and
* unbound, read-only and read-write properties. Write-only properties are
* not supported; connecting two read-only properties won't work;
* connecting two unbound properties doesn't make sense.
*
* If one of the bean properties fires a property change, this connector
* will set the other to the same value. If a bean property is read-only,
* the PropertyConnector will not listen to the other bean's property and so
* won't update the read-only property. And if a bean does not provide support
* for bound properties, it won't be observed.
* The properties must be single value bean properties as described by the
* Java
* Bean Secification.
*
* Constraints: the properties must be type compatible,
* i. e. values returned by one reader must be accepted by the other's writer,
* and vice versa.
*
* Examples:
* Note that the following examples are for demonstration purposes.
* The classes {@link BasicComponentFactory} and {@link Bindings}
* provide predefined connections for formatted text fields and combo boxes.
*
*
* // Connects a ValueModel and a JFormattedTextField
* JFormattedTextField textField = new JFormattedTextField();
* textField.setEditable(editable);
* PropertyConnector connector =
* PropertyConnector.connectAndUpdate(valueModel, "value", textField, "value");
*
* // Connects the boolean property "selectable" with a component enablement
* JComboBox comboBox = new JComboBox();
* ...
* PropertyConnector.connect(mainModel, "selectable", comboBox, "enabled");
*
*
* @author Karsten Lentzsch
* @version $Revision: 1.25 $
*
* @see PropertyChangeEvent
* @see PropertyChangeListener
* @see PropertyDescriptor
*/
public final class PropertyConnector {
/**
* Holds the first bean that in turn holds the first property.
*
* @see #getBean1()
*/
private final Object bean1;
/**
* Holds the second bean that in turn holds the second property.
*
* @see #getBean2()
*/
private final Object bean2;
/**
* Holds the class used to lookup methods for bean1.
* In a future version this may differ from bean1.getClass().
*/
private final Class> bean1Class;
/**
* Holds the class used to lookup methods for bean2.
* In a future version this may differ from bean2.getClass().
*/
private final Class> bean2Class;
/**
* Holds the first property name.
*
* @see #getProperty1Name()
*/
private final String property1Name;
/**
* Holds the second property name.
*
* @see #getProperty2Name()
*/
private final String property2Name;
/**
* The {@code PropertyChangeListener} used to handle
* changes in the first bean property.
*/
private final PropertyChangeListener property1ChangeHandler;
/**
* The {@code PropertyChangeListener} used to handle
* changes in the second bean property.
*/
private final PropertyChangeListener property2ChangeHandler;
/**
* Describes the accessor for property1; basically a getter and setter.
*/
private final PropertyAccessor property1Accessor;
/**
* Describes the accessor for property1; basically a getter and setter.
*/
private final PropertyAccessor property2Accessor;
// Instance creation ****************************************************
/**
* Constructs a PropertyConnector that synchronizes the two bound
* bean properties as specified by the given pairs of bean and associated
* property name.
* If {@code Bean1#property1Name} changes it updates
* {@code Bean2#property2Name} and vice versa.
* If a bean does not provide support for bound properties,
* changes will not be observed.
* If a bean property is read-only, this connector will not listen to
* the other bean's property and so won't update the read-only property.
*
* @param bean1 the bean that owns the first property
* @param property1Name the name of the first property
* @param bean2 the bean that owns the second property
* @param property2Name the name of the second property
* @throws NullPointerException
* if a bean or property name is {@code null}
* @throws IllegalArgumentException if the beans are identical and
* the property name are equal, or if both properties are read-only
*/
private PropertyConnector(Object bean1, String property1Name,
Object bean2, String property2Name) {
this.bean1 = checkNotNull(bean1, "Bean1 must not be null.");
this.bean2 = checkNotNull(bean2, "Bean2 must not be null.");
this.bean1Class = bean1.getClass();
this.bean2Class = bean2.getClass();
this.property1Name = checkNotBlank(property1Name, MUST_NOT_BE_BLANK, "propertyName1");
this.property2Name = checkNotBlank(property2Name, MUST_NOT_BE_BLANK, "propertyName2");
checkArgument(bean1 != bean2 || !property1Name.equals(property2Name),
"Cannot connect a bean property to itself on the same bean.");
property1Accessor = getPropertyAccessor(bean1Class, property1Name);
property2Accessor = getPropertyAccessor(bean2Class, property2Name);
// Used to check if property2 shall be observed,
// i.e. if a listener shall be registered with property2.
boolean property1Writable = property1Accessor.getWriteMethod() != null;
boolean property1Readable = property1Accessor.getReadMethod() != null;
// Reject write-only property1
checkArgument(!property1Writable || property1Readable,
"Property1 must be readable.");
// Used to check if property1 shall be observed,
// i.e. if a listener shall be registered with property1.
boolean property2Writable = property2Accessor.getWriteMethod() != null;
boolean property2Readable = property2Accessor.getReadMethod() != null;
// Reject write-only property2
checkArgument(!property2Writable || property2Readable,
"Property2 must be readable.");
// Reject to connect to read-only properties
checkArgument(property1Writable || property2Writable,
"Cannot connect two read-only properties.");
boolean property1Observable = BeanUtils.supportsBoundProperties(bean1Class);
boolean property2Observable = BeanUtils.supportsBoundProperties(bean2Class);
// We do not reject the case where two unobservable beans
// are connected; this allows a hand-update using #updateProperty1
// and #updateProperty2.
// Observe property1 if and only if bean1 provides support for
// bound bean properties, and if updates can be written to property2.
if (property1Observable && property2Writable) {
property1ChangeHandler = new PropertyChangeHandler(
bean1, property1Accessor, bean2, property2Accessor);
addPropertyChangeHandler(bean1, bean1Class, property1ChangeHandler);
} else {
property1ChangeHandler = null;
}
// Observe property2 if and only if bean2 provides support for
// bound bean properties, and if updates can be written to property1.
if (property2Observable && property1Writable) {
property2ChangeHandler = new PropertyChangeHandler(
bean2, property2Accessor, bean1, property1Accessor);
addPropertyChangeHandler(bean2, bean2Class, property2ChangeHandler);
} else {
property2ChangeHandler = null;
}
}
/**
* Synchronizes the two bound bean properties as specified
* by the given pairs of bean and associated property name.
* If {@code Bean1#property1Name} changes it updates
* {@code Bean2#property2Name} and vice versa.
* If a bean does not provide support for bound properties,
* changes will not be observed.
* If a bean property is read-only, this connector won't listen to
* the other bean's property and so won't update the read-only property.
*
* Returns the PropertyConnector that is required if one or the other
* property shall be updated.
*
* @param bean1 the bean that owns the first property
* @param property1Name the name of the first property
* @param bean2 the bean that owns the second property
* @param property2Name the name of the second property
* @return the PropertyConnector used to synchronize the properties,
* required if property1 or property2 shall be updated
*
* @throws NullPointerException
* if a bean or property name is {@code null}
* @throws IllegalArgumentException if the beans are identical and
* the property name are equal
*/
public static PropertyConnector connect(Object bean1, String property1Name,
Object bean2, String property2Name) {
return new PropertyConnector(bean1, property1Name,
bean2, property2Name);
}
/**
* Synchronizes the ValueModel with the specified bound bean property,
* and updates the bean immediately.
* If the ValueModel changes, it updates {@code Bean2#property2Name}
* and vice versa. If the bean doesn't provide support for bound properties,
* changes will not be observed.
* If the bean property is read-only, this connector will not listen
* to the ValueModel and so won't update the read-only property.
*
* @param valueModel the ValueModel that provides a bound value
* @param bean2 the bean that owns the second property
* @param property2Name the name of the second property
* @throws NullPointerException
* if the ValueModel, bean or property name is {@code null}
* @throws IllegalArgumentException if the bean is the ValueModel
* and the property name is {@code "value"}
*
* @since 2.0
*/
public static void connectAndUpdate(
ValueModel valueModel,
Object bean2, String property2Name) {
PropertyConnector connector = new PropertyConnector(
valueModel, "value", bean2, property2Name);
connector.updateProperty2();
}
// Property Accessors *****************************************************
/**
* Returns the Java Bean that holds the first property.
*
* @return the Bean that holds the first property
*/
public Object getBean1() {
return bean1;
}
/**
* Returns the Java Bean that holds the first property.
*
* @return the Bean that holds the first property
*/
public Object getBean2() {
return bean2;
}
/**
* Returns the name of the first Java Bean property.
*
* @return the name of the first property
*/
public String getProperty1Name() {
return property1Name;
}
/**
* Returns the name of the second Java Bean property.
*
* @return the name of the second property
*/
public String getProperty2Name() {
return property2Name;
}
// Sychronization *********************************************************
/**
* Reads the value of the second bean property and sets it as new
* value of the first bean property.
*
* @see #updateProperty2()
*/
public void updateProperty1() {
Object property2Value = property2Accessor.getValue(bean2);
setValueSilently(bean2, property2Accessor,
bean1, property1Accessor, property2Value);
}
/**
* Reads the value of the first bean property and sets it as new
* value of the second bean property.
*
* @see #updateProperty1()
*/
public void updateProperty2() {
Object property1Value = property1Accessor.getValue(bean1);
setValueSilently(bean1, property1Accessor,
bean2, property2Accessor, property1Value);
}
// Release ****************************************************************
/**
* Removes the PropertyChangeHandler from the observed bean,
* if the bean is not null and if property changes are not observed.
* This connector must not be used after calling {@code #release}.
*
* To avoid memory leaks it is recommended to invoke this method,
* if the connected beans live much longer than this connector.
*
* As an alternative you may use event listener lists in the connected
* beans that are implemented using {@code WeakReference}.
*
* @see java.lang.ref.WeakReference
*/
public void release() {
removePropertyChangeHandler(bean1, bean1Class, property1ChangeHandler);
removePropertyChangeHandler(bean2, bean2Class, property2ChangeHandler);
}
/**
* Used to add this class' PropertyChangeHandler to the given bean
* if it is not {@code null}. First checks if the bean class
* supports bound properties, i.e. it provides a pair of methods
* to register multicast property change event listeners;
* see section 7.4.1 of the Java Beans specification for details.
*
* @param bean the bean to add a property change listener
* @param listener the property change listener to be added
* @throws NullPointerException
* if the listener is {@code null}
* @throws PropertyUnboundException
* if the bean does not support bound properties
* @throws PropertyNotBindableException
* if the property change handler cannot be added successfully
*/
private static void addPropertyChangeHandler(
Object bean,
Class> beanClass,
PropertyChangeListener listener) {
if (bean != null) {
BeanUtils.addPropertyChangeListener(bean, beanClass, listener);
}
}
/**
* Used to remove this class' PropertyChangeHandler from the given bean
* if it is not {@code null}.
*
* @param bean the bean to remove the property change listener from
* @param listener the property change listener to be removed
* @throws PropertyUnboundException
* if the bean does not support bound properties
* @throws PropertyNotBindableException
* if the property change handler cannot be removed successfully
*/
private static void removePropertyChangeHandler(
Object bean,
Class> beanClass,
PropertyChangeListener listener) {
if (bean != null) {
BeanUtils.removePropertyChangeListener(bean, beanClass, listener);
}
}
// Helper Methods to Get and Set a Property Value *************************
private void setValueSilently(
Object sourceBean,
PropertyAccessor sourcePropertyAccessor,
Object targetBean,
PropertyAccessor targetPropertyAccessor,
Object newValue) {
Object targetValue = targetPropertyAccessor.getValue(targetBean);
if (targetValue == newValue) {
return;
}
if (property1ChangeHandler != null) {
removePropertyChangeHandler(bean1, bean1Class, property1ChangeHandler);
}
if (property2ChangeHandler != null) {
removePropertyChangeHandler(bean2, bean2Class, property2ChangeHandler);
}
try {
// Set the new value in the target bean.
targetPropertyAccessor.setValue(targetBean, newValue);
} catch (PropertyVetoException e) {
// Silently ignore this situation here, will be handled below.
}
// The target bean setter may have modified the new value.
// Read the value set in the target bean.
targetValue = targetPropertyAccessor.getValue(targetBean);
// If the new value and the value read differ,
// update the source bean's value.
// This ignores that the source bean setter may modify the value again.
// But we won't end in a loop.
if (!Objects.equals(targetValue, newValue)) {
boolean sourcePropertyWritable = sourcePropertyAccessor.getWriteMethod() != null;
if (sourcePropertyWritable) {
try {
sourcePropertyAccessor.setValue(sourceBean, targetValue);
} catch (PropertyVetoException e) {
// Ignore. The value set is a modified variant
// of a value that had been accepted before.
}
}
}
if (property1ChangeHandler != null) {
addPropertyChangeHandler(bean1, bean1Class, property1ChangeHandler);
}
if (property2ChangeHandler != null) {
addPropertyChangeHandler(bean2, bean2Class, property2ChangeHandler);
}
}
/**
* Looks up, lazily initializes and returns a {@code PropertyDescriptor}
* for the given Java Bean and property name.
*
* @param beanClass the Java Bean class used to lookup the property from
* @param propertyName the name of the property
* @return the descriptor for the given bean and property name
* @throws PropertyNotFoundException if the property could not be found
*/
private static PropertyAccessor getPropertyAccessor(
Class> beanClass,
String propertyName) {
return PropertyAccessors.getProvider().getAccessor(beanClass,
propertyName, null, null);
}
/**
* Listens to changes of a bean property and updates the property.
*/
private final class PropertyChangeHandler implements PropertyChangeListener {
/**
* Holds the bean that sends updates.
*/
private final Object sourceBean;
/**
* Holds the property descriptor for the bean to read from.
*/
private final PropertyAccessor sourcePropertyDescriptor;
/**
* Holds the bean to update.
*/
private final Object targetBean;
/**
* Holds the property descriptor for the bean to update.
*/
private final PropertyAccessor targetPropertyDescriptor;
private PropertyChangeHandler(
Object sourceBean,
PropertyAccessor sourcePropertyDescriptor,
Object targetBean,
PropertyAccessor targetPropertyDescriptor) {
this.sourceBean = sourceBean;
this.sourcePropertyDescriptor = sourcePropertyDescriptor;
this.targetBean = targetBean;
this.targetPropertyDescriptor = targetPropertyDescriptor;
}
/**
* A property in the observed bean has changed. First checks,
* if this listener should handle the event, because the event's
* property name is the one to be observed or the event indicates
* that any property may have changed. In case the event provides
* no new value, it is read from the source bean.
*
* @param evt the property change event to be handled
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
String sourcePropertyName = sourcePropertyDescriptor.getPropertyName();
String propertyName = evt.getPropertyName();
if (propertyName == null
|| propertyName.equals(sourcePropertyName)) {
Object newValue = evt.getNewValue();
if (newValue == null || propertyName == null) {
newValue = sourcePropertyDescriptor.getValue(sourceBean);
}
setValueSilently(sourceBean, sourcePropertyDescriptor,
targetBean, targetPropertyDescriptor,
newValue);
}
}
}
}