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: import java.lang.reflect.InvocationTargetException;
035: import java.lang.reflect.Method;
036:
037: /**
038: * Consists exclusively of static methods that provide
039: * convenience behavior for working with Java Bean properties.
040: *
041: * @author Karsten Lentzsch
042: * @version $Revision: 1.12 $
043: *
044: * @see Introspector
045: * @see BeanInfo
046: * @see PropertyDescriptor
047: */
048: public final class BeanUtils {
049:
050: private BeanUtils() {
051: // Override default constructor; prevents instantiation.
052: }
053:
054: /**
055: * Checks and answers whether the given class supports bound properties,
056: * i.e. it provides a pair of multicast event listener registration methods
057: * for <code>PropertyChangeListener</code>s:
058: * <pre>
059: * public void addPropertyChangeListener(PropertyChangeListener x);
060: * public void removePropertyChangeListener(PropertyChangeListener x);
061: * </pre>
062: *
063: * @param clazz the class to test
064: * @return true if the class supports bound properties, false otherwise
065: */
066: public static boolean supportsBoundProperties(Class<?> clazz) {
067: return (getPCLAdder(clazz) != null)
068: && (getPCLRemover(clazz) != null);
069: }
070:
071: /**
072: * Looks up and returns a <code>PropertyDescriptor</code> for the
073: * given Java Bean class and property name using the standard
074: * Java Bean introspection behavior.
075: *
076: * @param beanClass the type of the bean that holds the property
077: * @param propertyName the name of the Bean property
078: * @return the <code>PropertyDescriptor</code> associated with the given
079: * bean and property name as returned by the Bean introspection
080: *
081: * @throws IntrospectionException if an exception occurs during
082: * introspection.
083: * @throws NullPointerException if the beanClass or propertyName is <code>null</code>
084: *
085: * @since 1.1.1
086: */
087: public static PropertyDescriptor getPropertyDescriptor(
088: Class<?> beanClass, String propertyName)
089: throws IntrospectionException {
090:
091: BeanInfo info = Introspector.getBeanInfo(beanClass);
092: for (PropertyDescriptor element : info.getPropertyDescriptors()) {
093: if (propertyName.equals(element.getName())) {
094: return element;
095: }
096: }
097: throw new IntrospectionException("Property '" + propertyName
098: + "' not found in bean " + beanClass);
099: }
100:
101: /**
102: * Looks up and returns a <code>PropertyDescriptor</code> for the given
103: * Java Bean class and property name. If a getter name or setter name
104: * is available, these are used to create a PropertyDescriptor.
105: * Otherwise, the standard Java Bean introspection is used to determine
106: * the property descriptor.
107: *
108: * @param beanClass the class of the bean that holds the property
109: * @param propertyName the name of the property to be accessed
110: * @param getterName the optional name of the property's getter
111: * @param setterName the optional name of the property's setter
112: * @return the <code>PropertyDescriptor</code> associated with the
113: * given bean and property name
114: *
115: * @throws PropertyNotFoundException if the property could not be found
116: *
117: * @since 1.1.1
118: */
119: public static PropertyDescriptor getPropertyDescriptor(
120: Class<?> beanClass, String propertyName, String getterName,
121: String setterName) {
122: try {
123: return (getterName != null || setterName != null) ? new PropertyDescriptor(
124: propertyName, beanClass, getterName, setterName)
125: : getPropertyDescriptor(beanClass, propertyName);
126: } catch (IntrospectionException e) {
127: throw new PropertyNotFoundException(propertyName,
128: beanClass, e);
129: }
130: }
131:
132: /**
133: * Holds the class parameter list that is used to lookup
134: * the adder and remover methods for PropertyChangeListeners.
135: */
136: private static final Class<?>[] PCL_PARAMS = new Class<?>[] { PropertyChangeListener.class };
137:
138: /**
139: * Holds the class parameter list that is used to lookup
140: * the adder and remover methods for PropertyChangeListeners.
141: */
142: private static final Class<?>[] NAMED_PCL_PARAMS = new Class<?>[] {
143: String.class, PropertyChangeListener.class };
144:
145: /**
146: * Looks up and returns the method that adds a multicast
147: * PropertyChangeListener to instances of the given class.
148: *
149: * @param clazz the class that provides the adder method
150: * @return the method that adds multicast PropertyChangeListeners
151: */
152: public static Method getPCLAdder(Class<?> clazz) {
153: try {
154: return clazz.getMethod("addPropertyChangeListener",
155: PCL_PARAMS);
156: } catch (NoSuchMethodException e) {
157: return null;
158: }
159: }
160:
161: /**
162: * Looks up and returns the method that removes a multicast
163: * PropertyChangeListener from instances of the given class.
164: *
165: * @param clazz the class that provides the remover method
166: * @return the method that removes multicast PropertyChangeListeners
167: */
168: public static Method getPCLRemover(Class<?> clazz) {
169: try {
170: return clazz.getMethod("removePropertyChangeListener",
171: PCL_PARAMS);
172: } catch (NoSuchMethodException e) {
173: return null;
174: }
175: }
176:
177: /**
178: * Looks up and returns the method that adds a PropertyChangeListener
179: * for a specified property name to instances of the given class.
180: *
181: * @param clazz the class that provides the adder method
182: * @return the method that adds the PropertyChangeListeners
183: */
184: public static Method getNamedPCLAdder(Class<?> clazz) {
185: try {
186: return clazz.getMethod("addPropertyChangeListener",
187: NAMED_PCL_PARAMS);
188: } catch (NoSuchMethodException e) {
189: return null;
190: }
191: }
192:
193: /**
194: * Looks up and returns the method that removes a PropertyChangeListener
195: * for a specified property name from instances of the given class.
196: *
197: * @param clazz the class that provides the remover method
198: * @return the method that removes the PropertyChangeListeners
199: */
200: public static Method getNamedPCLRemover(Class<?> clazz) {
201: try {
202: return clazz.getMethod("removePropertyChangeListener",
203: NAMED_PCL_PARAMS);
204: } catch (NoSuchMethodException e) {
205: return null;
206: }
207: }
208:
209: /**
210: * Adds a property change listener to the given bean. First checks
211: * whether the bean supports <em>bound properties</em>, i.e. it provides
212: * a pair of methods to register multicast property change event listeners;
213: * see section 7.4.1 of the Java Beans specification for details.
214: *
215: * @param bean the bean to add the property change listener to
216: * @param beanClass the Bean class used to lookup methods from
217: * @param listener the listener to add
218: *
219: * @throws NullPointerException
220: * if the bean or listener is <code>null</code>
221: * @throws IllegalArgumentException
222: * if the bean is not an instance of the bean class
223: * @throws PropertyUnboundException
224: * if the bean does not support bound properties
225: * @throws PropertyNotBindableException
226: * if the property change handler cannot be added successfully
227: *
228: * @since 1.1.1
229: */
230: public static void addPropertyChangeListener(Object bean,
231: Class<?> beanClass, PropertyChangeListener listener) {
232: if (listener == null)
233: throw new NullPointerException(
234: "The listener must not be null.");
235: if (beanClass == null) {
236: beanClass = bean.getClass();
237: } else if (!beanClass.isInstance(bean)) {
238: throw new IllegalArgumentException("The bean " + bean
239: + " must be an instance of " + beanClass);
240: }
241:
242: if (bean instanceof Model) {
243: ((Model) bean).addPropertyChangeListener(listener);
244: return;
245: }
246:
247: // Check whether the bean supports bound properties.
248: if (!BeanUtils.supportsBoundProperties(beanClass))
249: throw new PropertyUnboundException(
250: "Bound properties unsupported by bean class="
251: + beanClass
252: + "\nThe Bean class must provide a pair of methods:"
253: + "\npublic void addPropertyChangeListener(PropertyChangeListener x);"
254: + "\npublic void removePropertyChangeListener(PropertyChangeListener x);");
255:
256: Method multicastPCLAdder = getPCLAdder(beanClass);
257: try {
258: multicastPCLAdder.invoke(bean, new Object[] { listener });
259: } catch (InvocationTargetException e) {
260: throw new PropertyNotBindableException(
261: "Due to an InvocationTargetException we failed to add "
262: + "a multicast PropertyChangeListener to bean: "
263: + bean, e.getCause());
264: } catch (IllegalAccessException e) {
265: throw new PropertyNotBindableException(
266: "Due to an IllegalAccessException we failed to add "
267: + "a multicast PropertyChangeListener to bean: "
268: + bean, e);
269: }
270: }
271:
272: /**
273: * Adds a named property change listener to the given bean. The bean
274: * must provide the optional support for listening on named properties
275: * as described in section 7.4.5 of the
276: * <a href="http://java.sun.com/products/javabeans/docs/spec.html">Java Bean
277: * Specification</a>. The bean class must provide the method:
278: * <pre>
279: * public void addPropertyChangeListener(String name, PropertyChangeListener l);
280: * </pre>
281: *
282: * @param bean the bean to add a property change handler
283: * @param beanClass the Bean class used to lookup methods from
284: * @param propertyName the name of the property to be observed
285: * @param listener the listener to add
286: *
287: * @throws NullPointerException
288: * if the bean, propertyName or listener is <code>null</code>
289: * @throws IllegalArgumentException
290: * if the bean is not an instance of the bean class
291: * @throws PropertyNotBindableException
292: * if the property change handler cannot be added successfully
293: */
294: public static void addPropertyChangeListener(Object bean,
295: Class<?> beanClass, String propertyName,
296: PropertyChangeListener listener) {
297:
298: if (propertyName == null)
299: throw new NullPointerException(
300: "The property name must not be null.");
301: if (listener == null)
302: throw new NullPointerException(
303: "The listener must not be null.");
304: if (beanClass == null) {
305: beanClass = bean.getClass();
306: } else if (!beanClass.isInstance(bean)) {
307: throw new IllegalArgumentException("The bean " + bean
308: + " must be an instance of " + beanClass);
309: }
310:
311: if (bean instanceof Model) {
312: ((Model) bean).addPropertyChangeListener(propertyName,
313: listener);
314: return;
315: }
316:
317: Method namedPCLAdder = getNamedPCLAdder(beanClass);
318: if (namedPCLAdder == null)
319: throw new PropertyNotBindableException(
320: "Could not find the bean method"
321: + "/npublic void addPropertyChangeListener(String, PropertyChangeListener);"
322: + "/nin bean:" + bean);
323:
324: try {
325: namedPCLAdder.invoke(bean, new Object[] { propertyName,
326: listener });
327: } catch (InvocationTargetException e) {
328: throw new PropertyNotBindableException(
329: "Due to an InvocationTargetException we failed to add "
330: + "a named PropertyChangeListener to bean: "
331: + bean, e.getCause());
332: } catch (IllegalAccessException e) {
333: throw new PropertyNotBindableException(
334: "Due to an IllegalAccessException we failed to add "
335: + "a named PropertyChangeListener to bean: "
336: + bean, e);
337: }
338: }
339:
340: /**
341: * Adds a property change listener to the given bean. First checks
342: * whether the bean supports <em>bound properties</em>, i.e. it provides
343: * a pair of methods to register multicast property change event listeners;
344: * see section 7.4.1 of the Java Beans specification for details.
345: *
346: * @param bean the bean to add the property change listener to
347: * @param listener the listener to add
348: *
349: * @throws NullPointerException
350: * if the bean or listener is <code>null</code>
351: * @throws PropertyUnboundException
352: * if the bean does not support bound properties
353: * @throws PropertyNotBindableException
354: * if the property change handler cannot be added successfully
355: */
356: public static void addPropertyChangeListener(Object bean,
357: PropertyChangeListener listener) {
358: addPropertyChangeListener(bean, bean.getClass(), listener);
359: }
360:
361: /**
362: * Adds a named property change listener to the given bean. The bean
363: * must provide the optional support for listening on named properties
364: * as described in section 7.4.5 of the
365: * <a href="http://java.sun.com/products/javabeans/docs/spec.html">Java Bean
366: * Specification</a>. The bean class must provide the method:
367: * <pre>
368: * public void addPropertyChangeListener(String name, PropertyChangeListener l);
369: * </pre>
370: *
371: * @param bean the bean to add a property change handler
372: * @param propertyName the name of the property to be observed
373: * @param listener the listener to add
374: *
375: * @throws NullPointerException
376: * if the bean, propertyName or listener is <code>null</code>
377: * @throws PropertyNotBindableException
378: * if the property change handler cannot be added successfully
379: */
380: public static void addPropertyChangeListener(Object bean,
381: String propertyName, PropertyChangeListener listener) {
382: addPropertyChangeListener(bean, bean.getClass(), propertyName,
383: listener);
384: }
385:
386: /**
387: * Removes a property change listener from the given bean.
388: *
389: * @param bean the bean to remove the property change listener from
390: * @param beanClass the Java Bean class used to lookup methods from
391: * @param listener the listener to remove
392: *
393: * @throws NullPointerException
394: * if the bean or listener is <code>null</code>
395: * @throws IllegalArgumentException
396: * if the bean is not an instance of the bean class
397: * @throws PropertyUnboundException
398: * if the bean does not support bound properties
399: * @throws PropertyNotBindableException
400: * if the property change handler cannot be removed successfully
401: *
402: * @since 1.1.1
403: */
404: public static void removePropertyChangeListener(Object bean,
405: Class<?> beanClass, PropertyChangeListener listener) {
406: if (listener == null)
407: throw new NullPointerException(
408: "The listener must not be null.");
409: if (beanClass == null) {
410: beanClass = bean.getClass();
411: } else if (!beanClass.isInstance(bean)) {
412: throw new IllegalArgumentException("The bean " + bean
413: + " must be an instance of " + beanClass);
414: }
415:
416: if (bean instanceof Model) {
417: ((Model) bean).removePropertyChangeListener(listener);
418: return;
419: }
420:
421: Method multicastPCLRemover = getPCLRemover(beanClass);
422: if (multicastPCLRemover == null)
423: throw new PropertyUnboundException(
424: "Could not find the method:"
425: + "\npublic void removePropertyChangeListener(String, PropertyChangeListener x);"
426: + "\nfor bean:" + bean);
427: try {
428: multicastPCLRemover.invoke(bean, new Object[] { listener });
429: } catch (InvocationTargetException e) {
430: throw new PropertyNotBindableException(
431: "Due to an InvocationTargetException we failed to remove "
432: + "a multicast PropertyChangeListener from bean: "
433: + bean, e.getCause());
434: } catch (IllegalAccessException e) {
435: throw new PropertyNotBindableException(
436: "Due to an IllegalAccessException we failed to remove "
437: + "a multicast PropertyChangeListener from bean: "
438: + bean, e);
439: }
440: }
441:
442: /**
443: * Removes a named property change listener from the given bean. The bean
444: * must provide the optional support for listening on named properties
445: * as described in section 7.4.5 of the
446: * <a href="http://java.sun.com/products/javabeans/docs/spec.html">Java Bean
447: * Specification</a>. The bean class must provide the method:
448: * <pre>
449: * public void removePropertyChangeHandler(String name, PropertyChangeListener l);
450: * </pre>
451: *
452: * @param bean the bean to remove the property change listener from
453: * @param beanClass the Java Bean class used to lookup methods from
454: * @param propertyName the name of the observed property
455: * @param listener the listener to remove
456: *
457: * @throws NullPointerException
458: * if the bean, propertyName, or listener is <code>null</code>
459: * @throws IllegalArgumentException
460: * if the bean is not an instance of the bean class
461: * @throws PropertyNotBindableException
462: * if the property change handler cannot be removed successfully
463: *
464: * @since 1.1.1
465: */
466: public static void removePropertyChangeListener(Object bean,
467: Class<?> beanClass, String propertyName,
468: PropertyChangeListener listener) {
469:
470: if (propertyName == null)
471: throw new NullPointerException(
472: "The property name must not be null.");
473: if (listener == null)
474: throw new NullPointerException(
475: "The listener must not be null.");
476: if (beanClass == null) {
477: beanClass = bean.getClass();
478: } else if (!beanClass.isInstance(bean)) {
479: throw new IllegalArgumentException("The bean " + bean
480: + " must be an instance of " + beanClass);
481: }
482:
483: if (bean instanceof Model) {
484: ((Model) bean).removePropertyChangeListener(propertyName,
485: listener);
486: return;
487: }
488:
489: Method namedPCLRemover = getNamedPCLRemover(beanClass);
490: if (namedPCLRemover == null)
491: throw new PropertyNotBindableException(
492: "Could not find the bean method"
493: + "/npublic void removePropertyChangeListener(String, PropertyChangeListener);"
494: + "/nin bean:" + bean);
495:
496: try {
497: namedPCLRemover.invoke(bean, new Object[] { propertyName,
498: listener });
499: } catch (InvocationTargetException e) {
500: throw new PropertyNotBindableException(
501: "Due to an InvocationTargetException we failed to remove "
502: + "a named PropertyChangeListener from bean: "
503: + bean, e.getCause());
504: } catch (IllegalAccessException e) {
505: throw new PropertyNotBindableException(
506: "Due to an IllegalAccessException we failed to remove "
507: + "a named PropertyChangeListener from bean: "
508: + bean, e);
509: }
510: }
511:
512: /**
513: * Removes a property change listener from the given bean.
514: *
515: * @param bean the bean to remove the property change listener from
516: * @param listener the listener to remove
517: * @throws NullPointerException if the bean or listener is <code>null</code>
518: * @throws PropertyUnboundException
519: * if the bean does not support bound properties
520: * @throws PropertyNotBindableException
521: * if the property change handler cannot be removed successfully
522: */
523: public static void removePropertyChangeListener(Object bean,
524: PropertyChangeListener listener) {
525: removePropertyChangeListener(bean, bean.getClass(), listener);
526: }
527:
528: /**
529: * Removes a named property change listener from the given bean. The bean
530: * must provide the optional support for listening on named properties
531: * as described in section 7.4.5 of the
532: * <a href="http://java.sun.com/products/javabeans/docs/spec.html">Java Bean
533: * Specification</a>. The bean class must provide the method:
534: * <pre>
535: * public void removePropertyChangeHandler(String name, PropertyChangeListener l);
536: * </pre>
537: *
538: * @param bean the bean to remove the property change listener from
539: * @param propertyName the name of the observed property
540: * @param listener the listener to remove
541: * @throws NullPointerException
542: * if the bean, propertyName, or listener is <code>null</code>
543: * @throws PropertyNotBindableException
544: * if the property change handler cannot be removed successfully
545: */
546: public static void removePropertyChangeListener(Object bean,
547: String propertyName, PropertyChangeListener listener) {
548: removePropertyChangeListener(bean, bean.getClass(),
549: propertyName, listener);
550: }
551:
552: // Getting and Setting Property Values ************************************
553:
554: /**
555: * Returns the value of the specified property of the given non-null bean.
556: * This operation is unsupported if the bean property is read-only.<p>
557: *
558: * If the read access fails, a PropertyAccessException is thrown
559: * that provides the Throwable that caused the failure.
560: *
561: * @param bean the bean to read the value from
562: * @param propertyDescriptor describes the property to be read
563: * @return the bean's property value
564: *
565: * @throws NullPointerException if the bean is <code>null</code>
566: * @throws UnsupportedOperationException if the bean property is write-only
567: * @throws PropertyAccessException if the new value could not be read
568: */
569: public static Object getValue(Object bean,
570: PropertyDescriptor propertyDescriptor) {
571: if (bean == null)
572: throw new NullPointerException("The bean must not be null.");
573:
574: Method getter = propertyDescriptor.getReadMethod();
575: if (getter == null) {
576: throw new UnsupportedOperationException("The property '"
577: + propertyDescriptor.getName() + "' is write-only.");
578: }
579:
580: try {
581: return getter.invoke(bean, (Object[]) null);
582: } catch (InvocationTargetException e) {
583: throw PropertyAccessException.createReadAccessException(
584: bean, propertyDescriptor, e.getCause());
585: } catch (IllegalAccessException e) {
586: throw PropertyAccessException.createReadAccessException(
587: bean, propertyDescriptor, e);
588: }
589: }
590:
591: /**
592: * Sets the given object as new value of the specified property of the given
593: * non-null bean. This is unsupported if the bean property is read-only.<p>
594: *
595: * If the write access fails, a PropertyAccessException is thrown
596: * that provides the Throwable that caused the failure.
597: * If the bean property is constrained and a VetoableChangeListener
598: * has vetoed against the value change, the PropertyAccessException
599: * wraps the PropertyVetoException thrown by the setter.
600: *
601: * @param bean the bean that holds the adapted property
602: * @param propertyDescriptor describes the property to be set
603: * @param newValue the property value to be set
604: *
605: * @throws NullPointerException if the bean is <code>null</code>
606: * @throws UnsupportedOperationException if the bean property is read-only
607: * @throws PropertyAccessException if the new value could not be set
608: * @throws PropertyVetoException if the bean setter throws this exception
609: */
610: public static void setValue(Object bean,
611: PropertyDescriptor propertyDescriptor, Object newValue)
612: throws PropertyVetoException {
613: if (bean == null)
614: throw new NullPointerException("The bean must not be null.");
615:
616: Method setter = propertyDescriptor.getWriteMethod();
617: if (setter == null) {
618: throw new UnsupportedOperationException("The property '"
619: + propertyDescriptor.getName() + "' is read-only.");
620: }
621: try {
622: setter.invoke(bean, new Object[] { newValue });
623: } catch (InvocationTargetException e) {
624: Throwable cause = e.getCause();
625: if (cause instanceof PropertyVetoException) {
626: throw (PropertyVetoException) cause;
627: }
628: throw PropertyAccessException.createWriteAccessException(
629: bean, newValue, propertyDescriptor, cause);
630:
631: } catch (IllegalAccessException e) {
632: throw PropertyAccessException.createWriteAccessException(
633: bean, newValue, propertyDescriptor, e);
634: } catch (IllegalArgumentException e) {
635: throw PropertyAccessException.createWriteAccessException(
636: bean, newValue, propertyDescriptor, e);
637: }
638: }
639:
640: }
|