/** * 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</code> * changes it updates <code>Bean2#property2Name</code> 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. * * <p>In case you don't need the PropertyConnector instance, you better use the static method * {@link #connect(Object, String, Object, String)}. This constructor may confuse developers if * you just use the side effects performed in the constructor; this is because it is quite * unconventional to instantiate an object that you never use. * * @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</code> * @throws IllegalArgumentException if the beans are identical and the property name are equal, or * if both properties are read-only */ public PropertyConnector(Object bean1, String property1Name, Object bean2, String property2Name) { if (bean1 == null) throw new NullPointerException("Bean1 must not be null."); if (bean2 == null) throw new NullPointerException("Bean2 must not be null."); if (property1Name == null) throw new NullPointerException("PropertyName1 must not be null."); if (property2Name == null) throw new NullPointerException("PropertyName2 must not be null."); if ((bean1 == bean2) && (property1Name.equals(property2Name))) throw new IllegalArgumentException( "Cannot connect a bean property to itself on the same bean."); this.bean1 = bean1; this.bean2 = bean2; this.bean1Class = bean1.getClass(); this.bean2Class = bean2.getClass(); this.property1Name = property1Name; this.property2Name = property2Name; property1Descriptor = getPropertyDescriptor(bean1Class, property1Name); property2Descriptor = getPropertyDescriptor(bean2Class, property2Name); // Used to check if property2 shall be observed, // i.e. if a listener shall be registered with property2. boolean property1Writable = property1Descriptor.getWriteMethod() != null; // Used to check if property1 shall be observed, // i.e. if a listener shall be registered with property1. boolean property2Writable = property2Descriptor.getWriteMethod() != null; // Reject to connect to read-only properties if (!property1Writable && !property2Writable) throw new IllegalArgumentException("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, property1Descriptor, bean2, property2Descriptor); 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, property2Descriptor, bean1, property1Descriptor); addPropertyChangeHandler(bean2, bean2Class, property2ChangeHandler); } else { property2ChangeHandler = null; } }
/** * Looks up, lazily initializes and returns a <code>PropertyDescriptor</code> 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 PropertyDescriptor getPropertyDescriptor(Class beanClass, String propertyName) { try { return BeanUtils.getPropertyDescriptor(beanClass, propertyName); } catch (IntrospectionException e) { throw new PropertyNotFoundException(propertyName, beanClass, e); } }
private void setValueSilently( Object sourceBean, PropertyDescriptor sourcePropertyDescriptor, Object targetBean, PropertyDescriptor targetPropertyDescriptor, Object newValue) { if (property1ChangeHandler != null) { removePropertyChangeHandler(bean1, bean1Class, property1ChangeHandler); } if (property2ChangeHandler != null) { removePropertyChangeHandler(bean2, bean2Class, property2ChangeHandler); } try { // Set the new value in the target bean. BeanUtils.setValue(targetBean, targetPropertyDescriptor, 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. Object value = BeanUtils.getValue(targetBean, targetPropertyDescriptor); // 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 (!BindingUtils.equals(value, newValue)) { boolean sourcePropertyWritable = sourcePropertyDescriptor.getWriteMethod() != null; if (sourcePropertyWritable) { try { BeanUtils.setValue(sourceBean, sourcePropertyDescriptor, value); } 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); } }
/** * 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 */ public void propertyChange(PropertyChangeEvent evt) { String sourcePropertyName = sourcePropertyDescriptor.getName(); if ((evt.getPropertyName() == null) || (evt.getPropertyName().equals(sourcePropertyName))) { Object newValue = evt.getNewValue(); if (newValue == null) { newValue = BeanUtils.getValue(sourceBean, sourcePropertyDescriptor); } setValueSilently( sourceBean, sourcePropertyDescriptor, targetBean, targetPropertyDescriptor, newValue); } }
/** * Used to remove this class' PropertyChangeHandler from the given bean if it is not <code>null * </code>. * * @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); } }
/** * 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 = BeanUtils.getValue(bean1, property1Descriptor); setValueSilently(bean1, property1Descriptor, bean2, property2Descriptor, property1Value); }