001: /*
002: * Copyright (c) 2002-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * o Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: *
010: * o Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * o Neither the name of JGoodies Karsten Lentzsch nor the names of
015: * its contributors may be used to endorse or promote products derived
016: * from this software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package com.jgoodies.binding.beans;
032:
033: import java.beans.*;
034:
035: import com.jgoodies.binding.BindingUtils;
036: import com.jgoodies.binding.value.ValueModel;
037:
038: /**
039: * Keeps two Java Bean properties in synch. This connector supports bound and
040: * unbound, read-only and read-write properties. Write-only properties are
041: * not supported; connecting two read-only properties won't work;
042: * connecting two unbound properties doesn't make sense.<p>
043: *
044: * If one of the bean properties fires a property change, this connector
045: * will set the other to the same value. If a bean property is read-only,
046: * the PropertyConnector will not listen to the other bean's property and so
047: * won't update the read-only property. And if a bean does not provide support
048: * for bound properties, it won't be observed.
049: * The properties must be single value bean properties as described by the
050: * <a href="http://java.sun.com/products/javabeans/docs/spec.html">Java
051: * Bean Secification</a>.<p>
052: *
053: * <strong>Constraints:</strong> the properties must be type compatible,
054: * i. e. values returned by one reader must be accepted by the other's writer,
055: * and vice versa.<p>
056: *
057: * <strong>Examples:</strong><pre>
058: * // Connects a ValueModel and a JFormattedTextField
059: * JFormattedTextField textField = new JFormattedTextField();
060: * textField.setEditable(editable);
061: * PropertyConnector connector =
062: * new PropertyConnector(valueModel, "value", textField, "value");
063: * connector.updateProperty2();
064: *
065: * // Connects the boolean property "selectable" with a component enablement
066: * JComboBox comboBox = new JComboBox();
067: * ...
068: * new PropertyConnector(mainModel, "selectable", comboBox, "enabled");
069: * </pre>
070: *
071: * @author Karsten Lentzsch
072: * @version $Revision: 1.16 $
073: *
074: * @see PropertyChangeEvent
075: * @see PropertyChangeListener
076: * @see PropertyDescriptor
077: */
078: public final class PropertyConnector {
079:
080: /**
081: * Holds the first bean that in turn holds the first property.
082: *
083: * @see #getBean1()
084: */
085: private final Object bean1;
086:
087: /**
088: * Holds the second bean that in turn holds the second property.
089: *
090: * @see #getBean2()
091: */
092: private final Object bean2;
093:
094: /**
095: * Holds the class used to lookup methods for bean1.
096: * In a future version this may differ from bean1.getClass().
097: */
098: private final Class<?> bean1Class;
099:
100: /**
101: * Holds the class used to lookup methods for bean2.
102: * In a future version this may differ from bean2.getClass().
103: */
104: private final Class<?> bean2Class;
105:
106: /**
107: * Holds the first property name.
108: *
109: * @see #getProperty1Name()
110: */
111: private final String property1Name;
112:
113: /**
114: * Holds the second property name.
115: *
116: * @see #getProperty2Name()
117: */
118: private final String property2Name;
119:
120: /**
121: * The <code>PropertyChangeListener</code> used to handle
122: * changes in the first bean property.
123: */
124: private final PropertyChangeListener property1ChangeHandler;
125:
126: /**
127: * The <code>PropertyChangeListener</code> used to handle
128: * changes in the second bean property.
129: */
130: private final PropertyChangeListener property2ChangeHandler;
131:
132: /**
133: * Describes the accessor for property1; basically a getter and setter.
134: */
135: private final PropertyDescriptor property1Descriptor;
136:
137: /**
138: * Describes the accessor for property1; basically a getter and setter.
139: */
140: private final PropertyDescriptor property2Descriptor;
141:
142: // Instance creation ****************************************************
143:
144: /**
145: * Constructs a PropertyConnector that synchronizes the two bound
146: * bean properties as specified by the given pairs of bean and associated
147: * property name.
148: * If <code>Bean1#property1Name</code> changes it updates
149: * <code>Bean2#property2Name</code> and vice versa.
150: * If a bean does not provide support for bound properties,
151: * changes will not be observed.
152: * If a bean property is read-only, this connector will not listen to
153: * the other bean's property and so won't update the read-only property.
154: *
155: * @param bean1 the bean that owns the first property
156: * @param property1Name the name of the first property
157: * @param bean2 the bean that owns the second property
158: * @param property2Name the name of the second property
159: * @throws NullPointerException
160: * if a bean or property name is <code>null</code>
161: * @throws IllegalArgumentException if the beans are identical and
162: * the property name are equal, or if both properties are read-only
163: */
164: private PropertyConnector(Object bean1, String property1Name,
165: Object bean2, String property2Name) {
166: if (bean1 == null)
167: throw new NullPointerException("Bean1 must not be null.");
168: if (bean2 == null)
169: throw new NullPointerException("Bean2 must not be null.");
170: if (property1Name == null)
171: throw new NullPointerException(
172: "PropertyName1 must not be null.");
173: if (property2Name == null)
174: throw new NullPointerException(
175: "PropertyName2 must not be null.");
176: if ((bean1 == bean2) && (property1Name.equals(property2Name)))
177: throw new IllegalArgumentException(
178: "Cannot connect a bean property to itself on the same bean.");
179:
180: this .bean1 = bean1;
181: this .bean2 = bean2;
182: this .bean1Class = bean1.getClass();
183: this .bean2Class = bean2.getClass();
184: this .property1Name = property1Name;
185: this .property2Name = property2Name;
186:
187: property1Descriptor = getPropertyDescriptor(bean1Class,
188: property1Name);
189: property2Descriptor = getPropertyDescriptor(bean2Class,
190: property2Name);
191:
192: // Used to check if property2 shall be observed,
193: // i.e. if a listener shall be registered with property2.
194: boolean property1Writable = property1Descriptor
195: .getWriteMethod() != null;
196: boolean property1Readable = property1Descriptor.getReadMethod() != null;
197:
198: // Reject write-only property1
199: if (property1Writable && !property1Readable) {
200: throw new IllegalArgumentException(
201: "Property1 must be readable.");
202: }
203:
204: // Used to check if property1 shall be observed,
205: // i.e. if a listener shall be registered with property1.
206: boolean property2Writable = property2Descriptor
207: .getWriteMethod() != null;
208: boolean property2Readable = property2Descriptor.getReadMethod() != null;
209:
210: // Reject write-only property2
211: if (property2Writable && !property2Readable) {
212: throw new IllegalArgumentException(
213: "Property2 must be readable.");
214: }
215: // Reject to connect to read-only properties
216: if (!property1Writable && !property2Writable)
217: throw new IllegalArgumentException(
218: "Cannot connect two read-only properties.");
219:
220: boolean property1Observable = BeanUtils
221: .supportsBoundProperties(bean1Class);
222: boolean property2Observable = BeanUtils
223: .supportsBoundProperties(bean2Class);
224: // We do not reject the case where two unobservable beans
225: // are connected; this allows a hand-update using #updateProperty1
226: // and #updateProperty2.
227:
228: // Observe property1 if and only if bean1 provides support for
229: // bound bean properties, and if updates can be written to property2.
230: if (property1Observable && property2Writable) {
231: property1ChangeHandler = new PropertyChangeHandler(bean1,
232: property1Descriptor, bean2, property2Descriptor);
233: addPropertyChangeHandler(bean1, bean1Class,
234: property1ChangeHandler);
235: } else {
236: property1ChangeHandler = null;
237: }
238:
239: // Observe property2 if and only if bean2 provides support for
240: // bound bean properties, and if updates can be written to property1.
241: if (property2Observable && property1Writable) {
242: property2ChangeHandler = new PropertyChangeHandler(bean2,
243: property2Descriptor, bean1, property1Descriptor);
244: addPropertyChangeHandler(bean2, bean2Class,
245: property2ChangeHandler);
246: } else {
247: property2ChangeHandler = null;
248: }
249: }
250:
251: /**
252: * Synchronizes the two bound bean properties as specified
253: * by the given pairs of bean and associated property name.
254: * If <code>Bean1#property1Name</code> changes it updates
255: * <code>Bean2#property2Name</code> and vice versa.
256: * If a bean does not provide support for bound properties,
257: * changes will not be observed.
258: * If a bean property is read-only, this connector won't listen to
259: * the other bean's property and so won't update the read-only property.<p>
260: *
261: * Returns the PropertyConnector that is required if one or the other
262: * property shall be updated.
263: *
264: * @param bean1 the bean that owns the first property
265: * @param property1Name the name of the first property
266: * @param bean2 the bean that owns the second property
267: * @param property2Name the name of the second property
268: * @return the PropertyConnector used to synchronize the properties,
269: * required if property1 or property2 shall be updated
270: *
271: * @throws NullPointerException
272: * if a bean or property name is <code>null</code>
273: * @throws IllegalArgumentException if the beans are identical and
274: * the property name are equal
275: */
276: public static PropertyConnector connect(Object bean1,
277: String property1Name, Object bean2, String property2Name) {
278: return new PropertyConnector(bean1, property1Name, bean2,
279: property2Name);
280: }
281:
282: /**
283: * Synchronizes the ValueModel with the specified bound bean property,
284: * and updates the bean immediately.
285: * If the ValueModel changes, it updates <code>Bean2#property2Name</code>
286: * and vice versa. If the bean doesn't provide support for bound properties,
287: * changes will not be observed.
288: * If the bean property is read-only, this connector will not listen
289: * to the ValueModel and so won't update the read-only property.
290: *
291: * @param valueModel the ValueModel that provides a bound value
292: * @param bean2 the bean that owns the second property
293: * @param property2Name the name of the second property
294: * @throws NullPointerException
295: * if the ValueModel, bean or property name is <code>null</code>
296: * @throws IllegalArgumentException if the bean is the ValueModel
297: * and the property name is <code>"value"</code>
298: *
299: * @since 2.0
300: */
301: public static void connectAndUpdate(ValueModel valueModel,
302: Object bean2, String property2Name) {
303: PropertyConnector connector = new PropertyConnector(valueModel,
304: "value", bean2, property2Name);
305: connector.updateProperty2();
306: }
307:
308: // Property Accessors *****************************************************
309:
310: /**
311: * Returns the Java Bean that holds the first property.
312: *
313: * @return the Bean that holds the first property
314: */
315: public Object getBean1() {
316: return bean1;
317: }
318:
319: /**
320: * Returns the Java Bean that holds the first property.
321: *
322: * @return the Bean that holds the first property
323: */
324: public Object getBean2() {
325: return bean2;
326: }
327:
328: /**
329: * Returns the name of the first Java Bean property.
330: *
331: * @return the name of the first property
332: */
333: public String getProperty1Name() {
334: return property1Name;
335: }
336:
337: /**
338: * Returns the name of the second Java Bean property.
339: *
340: * @return the name of the second property
341: */
342: public String getProperty2Name() {
343: return property2Name;
344: }
345:
346: // Sychronization *********************************************************
347:
348: /**
349: * Reads the value of the second bean property and sets it as new
350: * value of the first bean property.
351: *
352: * @see #updateProperty2()
353: */
354: public void updateProperty1() {
355: Object property2Value = BeanUtils.getValue(bean2,
356: property2Descriptor);
357: setValueSilently(bean2, property2Descriptor, bean1,
358: property1Descriptor, property2Value);
359: }
360:
361: /**
362: * Reads the value of the first bean property and sets it as new
363: * value of the second bean property.
364: *
365: * @see #updateProperty1()
366: */
367: public void updateProperty2() {
368: Object property1Value = BeanUtils.getValue(bean1,
369: property1Descriptor);
370: setValueSilently(bean1, property1Descriptor, bean2,
371: property2Descriptor, property1Value);
372: }
373:
374: // Release ****************************************************************
375:
376: /**
377: * Removes the PropertyChangeHandler from the observed bean,
378: * if the bean is not null and if property changes are not observed.
379: * This connector must not be used after calling <code>#release</code>.<p>
380: *
381: * To avoid memory leaks it is recommended to invoke this method,
382: * if the connected beans live much longer than this connector.<p>
383: *
384: * As an alternative you may use event listener lists in the connected
385: * beans that are implemented using <code>WeakReference</code>.
386: *
387: * @see java.lang.ref.WeakReference
388: */
389: public void release() {
390: removePropertyChangeHandler(bean1, bean1Class,
391: property1ChangeHandler);
392: removePropertyChangeHandler(bean2, bean2Class,
393: property2ChangeHandler);
394: }
395:
396: /**
397: * Used to add this class' PropertyChangeHandler to the given bean
398: * if it is not <code>null</code>. First checks if the bean class
399: * supports <em>bound properties</em>, i.e. it provides a pair of methods
400: * to register multicast property change event listeners;
401: * see section 7.4.1 of the Java Beans specification for details.
402: *
403: * @param bean the bean to add a property change listener
404: * @param listener the property change listener to be added
405: * @throws NullPointerException
406: * if the listener is <code>null</code>
407: * @throws PropertyUnboundException
408: * if the bean does not support bound properties
409: * @throws PropertyNotBindableException
410: * if the property change handler cannot be added successfully
411: */
412: private static void addPropertyChangeHandler(Object bean,
413: Class<?> beanClass, PropertyChangeListener listener) {
414: if (bean != null) {
415: BeanUtils.addPropertyChangeListener(bean, beanClass,
416: listener);
417: }
418: }
419:
420: /**
421: * Used to remove this class' PropertyChangeHandler from the given bean
422: * if it is not <code>null</code>.
423: *
424: * @param bean the bean to remove the property change listener from
425: * @param listener the property change listener to be removed
426: * @throws PropertyUnboundException
427: * if the bean does not support bound properties
428: * @throws PropertyNotBindableException
429: * if the property change handler cannot be removed successfully
430: */
431: private static void removePropertyChangeHandler(Object bean,
432: Class<?> beanClass, PropertyChangeListener listener) {
433: if (bean != null) {
434: BeanUtils.removePropertyChangeListener(bean, beanClass,
435: listener);
436: }
437: }
438:
439: // Helper Methods to Get and Set a Property Value *************************
440:
441: private void setValueSilently(Object sourceBean,
442: PropertyDescriptor sourcePropertyDescriptor,
443: Object targetBean,
444: PropertyDescriptor targetPropertyDescriptor, Object newValue) {
445: Object targetValue = BeanUtils.getValue(targetBean,
446: targetPropertyDescriptor);
447: if (targetValue == newValue) {
448: return;
449: }
450: if (property1ChangeHandler != null) {
451: removePropertyChangeHandler(bean1, bean1Class,
452: property1ChangeHandler);
453: }
454: if (property2ChangeHandler != null) {
455: removePropertyChangeHandler(bean2, bean2Class,
456: property2ChangeHandler);
457: }
458: try {
459: // Set the new value in the target bean.
460: BeanUtils.setValue(targetBean, targetPropertyDescriptor,
461: newValue);
462: } catch (PropertyVetoException e) {
463: // Silently ignore this situation here, will be handled below.
464: }
465: // The target bean setter may have modified the new value.
466: // Read the value set in the target bean.
467: targetValue = BeanUtils.getValue(targetBean,
468: targetPropertyDescriptor);
469: // If the new value and the value read differ,
470: // update the source bean's value.
471: // This ignores that the source bean setter may modify the value again.
472: // But we won't end in a loop.
473: if (!BindingUtils.equals(targetValue, newValue)) {
474: boolean sourcePropertyWritable = sourcePropertyDescriptor
475: .getWriteMethod() != null;
476: if (sourcePropertyWritable) {
477: try {
478: BeanUtils.setValue(sourceBean,
479: sourcePropertyDescriptor, targetValue);
480: } catch (PropertyVetoException e) {
481: // Ignore. The value set is a modified variant
482: // of a value that had been accepted before.
483: }
484: }
485: }
486: if (property1ChangeHandler != null) {
487: addPropertyChangeHandler(bean1, bean1Class,
488: property1ChangeHandler);
489: }
490: if (property2ChangeHandler != null) {
491: addPropertyChangeHandler(bean2, bean2Class,
492: property2ChangeHandler);
493: }
494: }
495:
496: /**
497: * Looks up, lazily initializes and returns a <code>PropertyDescriptor</code>
498: * for the given Java Bean and property name.
499: *
500: * @param beanClass the Java Bean class used to lookup the property from
501: * @param propertyName the name of the property
502: * @return the descriptor for the given bean and property name
503: * @throws PropertyNotFoundException if the property could not be found
504: */
505: private static PropertyDescriptor getPropertyDescriptor(
506: Class<?> beanClass, String propertyName) {
507: try {
508: return BeanUtils.getPropertyDescriptor(beanClass,
509: propertyName);
510: } catch (IntrospectionException e) {
511: throw new PropertyNotFoundException(propertyName,
512: beanClass, e);
513: }
514: }
515:
516: /**
517: * Listens to changes of a bean property and updates the property.
518: */
519: private final class PropertyChangeHandler implements
520: PropertyChangeListener {
521:
522: /**
523: * Holds the bean that sends updates.
524: */
525: private final Object sourceBean;
526:
527: /**
528: * Holds the property descriptor for the bean to read from.
529: */
530: private final PropertyDescriptor sourcePropertyDescriptor;
531:
532: /**
533: * Holds the bean to update.
534: */
535: private final Object targetBean;
536:
537: /**
538: * Holds the property descriptor for the bean to update.
539: */
540: private final PropertyDescriptor targetPropertyDescriptor;
541:
542: private PropertyChangeHandler(Object sourceBean,
543: PropertyDescriptor sourcePropertyDescriptor,
544: Object targetBean,
545: PropertyDescriptor targetPropertyDescriptor) {
546: this .sourceBean = sourceBean;
547: this .sourcePropertyDescriptor = sourcePropertyDescriptor;
548: this .targetBean = targetBean;
549: this .targetPropertyDescriptor = targetPropertyDescriptor;
550: }
551:
552: /**
553: * A property in the observed bean has changed. First checks,
554: * if this listener should handle the event, because the event's
555: * property name is the one to be observed or the event indicates
556: * that any property may have changed. In case the event provides
557: * no new value, it is read from the source bean.
558: *
559: * @param evt the property change event to be handled
560: */
561: public void propertyChange(PropertyChangeEvent evt) {
562: String sourcePropertyName = sourcePropertyDescriptor
563: .getName();
564: if ((evt.getPropertyName() == null)
565: || (evt.getPropertyName()
566: .equals(sourcePropertyName))) {
567: Object newValue = evt.getNewValue();
568: if (newValue == null) {
569: newValue = BeanUtils.getValue(sourceBean,
570: sourcePropertyDescriptor);
571: }
572: setValueSilently(sourceBean, sourcePropertyDescriptor,
573: targetBean, targetPropertyDescriptor, newValue);
574: }
575: }
576:
577: }
578:
579: }
|