001: /* *************************************************************************
002:
003: Millstone(TM)
004: Open Sourced User Interface Library for
005: Internet Development with Java
006:
007: Millstone is a registered trademark of IT Mill Ltd
008: Copyright (C) 2000-2005 IT Mill Ltd
009:
010: *************************************************************************
011:
012: This library is free software; you can redistribute it and/or
013: modify it under the terms of the GNU Lesser General Public
014: license version 2.1 as published by the Free Software Foundation.
015:
016: This library is distributed in the hope that it will be useful,
017: but WITHOUT ANY WARRANTY; without even the implied warranty of
018: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
019: Lesser General Public License for more details.
020:
021: You should have received a copy of the GNU Lesser General Public
022: License along with this library; if not, write to the Free Software
023: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
024:
025: *************************************************************************
026:
027: For more information, contact:
028:
029: IT Mill Ltd phone: +358 2 4802 7180
030: Ruukinkatu 2-4 fax: +358 2 4802 7181
031: 20540, Turku email: info@itmill.com
032: Finland company www: www.itmill.com
033:
034: Primary source for MillStone information and releases: www.millstone.org
035:
036: ********************************************************************** */
037:
038: package org.millstone.base.data.util;
039:
040: import java.lang.reflect.InvocationTargetException;
041: import java.lang.reflect.Method;
042: import java.lang.reflect.Constructor;
043: import java.util.LinkedList;
044: import org.millstone.base.data.Property;
045:
046: /** <p>Proxy class for creating Properties from pairs of getter and setter
047: * methods of a Bean property. An instance of this class can be thought as
048: * having been attached to a field of an object. Accessing the object
049: * through the Property interface directly manipulates the underlying
050: * field.</p>
051: *
052: * <p>It's assumed that the return value returned by the getter method
053: * is assignable to the type of the property, and the setter method
054: * parameter is assignable to that value.</p>
055: *
056: * <p>A valid getter method must always be available, but instance of this
057: * class can be constructed with a <code>null</code> setter method in which
058: * case the resulting MethodProperty is read-only.</p>
059: *
060: * @author IT Mill Ltd.
061: * @version 3.1.1
062: * @since 3.0
063: */
064: public class MethodProperty implements Property {
065:
066: /** The object that includes the property the MethodProperty is bound to */
067: private Object instance;
068:
069: /** Argument arrays for the getter and setter methods */
070: private Object[] setArgs, getArgs;
071:
072: /** Is the MethodProperty read-only? */
073: private boolean readOnly;
074:
075: /** The getter and setter methods */
076: private Method setMethod, getMethod;
077:
078: /** Index of the new value in the argument list for the setter method.
079: * If the setter method requires several parameters, this index tells
080: * which one is the actual value to change.
081: */
082: private int setArgumentIndex;
083:
084: /** Type of the property */
085: private Class type;
086:
087: /** List of listeners who are interested in the read-only status changes
088: * of the MethodProperty
089: */
090: private LinkedList readOnlyStatusChangeListeners = null;
091:
092: /** <p>Creates a new instance of MethodProperty from a named bean
093: * property. This constructor takes an object and the name of a bean
094: * property and initializes itself with the accessor methods for the
095: * property. The getter method of a MethodProperty instantiated
096: * with this constructor will be called with no arguments, and the
097: * setter method with only the new value as the sole argument.</p>
098: *
099: * <p>If the setter method is unavailable, the resulting MethodProperty
100: * will be read-only, otherwise it will be read-write.</p>
101: *
102: * <p>Method names are constucted from the bean property by adding
103: * get/is/are/set prefix and capitalising the first character in the
104: * name of the given bean property</p>
105: *
106: * @param instance object that includes the property
107: * @param beanPropertyName name of the property to bind to
108: */
109: public MethodProperty(Object instance, String beanPropertyName) {
110:
111: Class beanClass = instance.getClass();
112:
113: // Assure that the first letter is upper cased (it is a common
114: // mistake to write firstName, not FirstName).
115: if (Character.isLowerCase(beanPropertyName.charAt(0))) {
116: char[] buf = beanPropertyName.toCharArray();
117: buf[0] = Character.toUpperCase(buf[0]);
118: beanPropertyName = new String(buf);
119: }
120:
121: // Find the get method
122: getMethod = null;
123: try {
124: getMethod = beanClass.getMethod("get" + beanPropertyName,
125: new Class[] {});
126: } catch (java.lang.NoSuchMethodException ignored) {
127: try {
128: getMethod = beanClass.getMethod(
129: "is" + beanPropertyName, new Class[] {});
130: } catch (java.lang.NoSuchMethodException ignoredAsWell) {
131: try {
132: getMethod = beanClass.getMethod("are"
133: + beanPropertyName, new Class[] {});
134: } catch (java.lang.NoSuchMethodException e) {
135: throw new MethodProperty.MethodException(
136: "Bean property " + beanPropertyName
137: + " can not be found");
138: }
139: }
140: }
141:
142: // In case the get method is found, resolve the type
143: type = getMethod.getReturnType();
144:
145: // Find the set method
146: setMethod = null;
147: try {
148: setMethod = beanClass.getMethod("set" + beanPropertyName,
149: new Class[] { type });
150: } catch (java.lang.NoSuchMethodException skipped) {
151: }
152:
153: // Get the return type from get method
154: if (type.isPrimitive()) {
155: if (type.equals(Boolean.TYPE))
156: type = Boolean.class;
157: else if (type.equals(Integer.TYPE))
158: type = Integer.class;
159: else if (type.equals(Float.TYPE))
160: type = Float.class;
161: else if (type.equals(Double.TYPE))
162: type = Double.class;
163: else if (type.equals(Byte.TYPE))
164: type = Byte.class;
165: else if (type.equals(Character.TYPE))
166: type = Character.class;
167: else if (type.equals(Short.TYPE))
168: type = Short.class;
169: else if (type.equals(Long.TYPE))
170: type = Long.class;
171: }
172:
173: setArguments(new Object[] {}, new Object[] { null }, 0);
174: this .readOnly = (setMethod == null);
175: this .instance = instance;
176: }
177:
178: /** <p>Creates a new instance of MethodProperty from named getter and
179: * setter methods. The getter method of a MethodProperty instantiated
180: * with this constructor will be called with no arguments, and the
181: * setter method with only the new value as the sole argument.</p>
182: *
183: * <p>If the setter method is <code>null</code>, the resulting
184: * MethodProperty will be read-only, otherwise it will be
185: * read-write.</p>
186: *
187: * @param type type of the property
188: * @param instance object that includes the property
189: * @param getMethodName name of the getter method
190: * @param setMethodName name of the setter method
191: *
192: */
193: public MethodProperty(Class type, Object instance,
194: String getMethodName, String setMethodName) {
195: this (type, instance, getMethodName, setMethodName,
196: new Object[] {}, new Object[] { null }, 0);
197: }
198:
199: /** <p>Creates a new instance of MethodProperty with the getter and
200: * setter methods. The getter method of a MethodProperty instantiated
201: * with this constructor will be called with no arguments, and the
202: * setter method with only the new value as the sole argument.</p>
203: *
204: * <p>If the setter method is <code>null</code>, the resulting
205: * MethodProperty will be read-only, otherwise it will be
206: * read-write.</p>
207: *
208: * @param type type of the property
209: * @param instance object that includes the property
210: * @param getMethod the getter method
211: * @param setMethod the setter method
212: */
213: public MethodProperty(Class type, Object instance,
214: Method getMethod, Method setMethod) {
215: this (type, instance, getMethod, setMethod, new Object[] {},
216: new Object[] { null }, 0);
217: }
218:
219: /** <p>Creates a new instance of MethodProperty from named getter and
220: * setter methods and argument lists. The getter method of a
221: * MethodProperty instantiated with this constructor will be called with
222: * <code>getArgs</code> as arguments. <code>setArgs</code> will be used
223: * as the arguments for the setter method, though the argument indexed
224: * by <code>setArgumentIndex</code> will be replaced with the argument
225: * passed to the {@link #setValue(Object newValue)} method.</p>
226: *
227: * <p>For example, if the <code>setArgs</code> contains <code>A</code>,
228: * <code>B</code> and <code>C</code>, and <code>setArgumentIndex =
229: * 1</code>, the call <code>methodProperty.setValue(X)</code> would
230: * result in the setter method to be called with the parameter set of
231: * <code>{A, X, C}</code></p>
232: *
233: * @param type type of the property
234: * @param instance object that includes the property
235: * @param getMethodName the name of the getter method
236: * @param setMethodName the name of the setter method
237: * @param getArgs fixed argument list to be passed to the getter method
238: * @param setArgs fixed argument list to be passed to the setter method
239: * @param setArgumentIndex the index of the argument in
240: * <code>setArgs</code> to be replaced with <code>newValue</code> when
241: * {@link #setValue(Object newValue)} is called
242: */
243: public MethodProperty(Class type, Object instance,
244: String getMethodName, String setMethodName,
245: Object[] getArgs, Object[] setArgs, int setArgumentIndex) {
246:
247: // Check the setargs and setargs index
248: if (setMethodName != null && setArgs == null)
249: throw new IndexOutOfBoundsException(
250: "The setArgs can not be null");
251: if (setMethodName != null
252: && (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length))
253: throw new IndexOutOfBoundsException(
254: "The setArgumentIndex must be >= 0 and < setArgs.length");
255:
256: // Set type
257: this .type = type;
258:
259: // Find set and get -methods
260: Method[] m = instance.getClass().getMethods();
261:
262: // Find get method
263: boolean found = false;
264: for (int i = 0; i < m.length; i++) {
265:
266: // Test the name of the get Method
267: if (!m[i].getName().equals(getMethodName)) {
268:
269: // name does not match, try next method
270: continue;
271: }
272:
273: // Test return type
274: if (!type.equals(m[i].getReturnType()))
275: continue;
276:
277: // Test the parameter types
278: Class[] c = m[i].getParameterTypes();
279: if (c.length != getArgs.length) {
280:
281: // not the right amount of parameters, try next method
282: continue;
283: }
284: int j = 0;
285: while (j < c.length) {
286: if (getArgs[j] != null
287: && !c[j]
288: .isAssignableFrom(getArgs[j].getClass())) {
289:
290: // parameter type does not match, try next method
291: break;
292: }
293: j++;
294: }
295: if (j == c.length) {
296:
297: // all paramteters matched
298: if (found == true) {
299: throw new MethodProperty.MethodException(
300: "Could not uniquely identify "
301: + getMethodName + "-method");
302: } else {
303: found = true;
304: getMethod = m[i];
305: }
306: }
307: }
308: if (found != true) {
309: throw new MethodProperty.MethodException("Could not find "
310: + getMethodName + "-method");
311: }
312:
313: // Find set method
314: if (setMethodName != null) {
315:
316: // Find setMethod
317: found = false;
318: for (int i = 0; i < m.length; i++) {
319:
320: // Check name
321: if (!m[i].getName().equals(setMethodName)) {
322:
323: // name does not match, try next method
324: continue;
325: }
326:
327: // Check parameter compatibility
328: Class[] c = m[i].getParameterTypes();
329: if (c.length != setArgs.length) {
330:
331: // not the right amount of parameters, try next method
332: continue;
333: }
334: int j = 0;
335: while (j < c.length) {
336: if (setArgs[j] != null
337: && !c[j].isAssignableFrom(setArgs[j]
338: .getClass())) {
339:
340: // parameter type does not match, try next method
341: break;
342: } else if (j == setArgumentIndex
343: && !c[j].equals(type)) {
344:
345: // Property type is not the same as setArg type
346: break;
347: }
348: j++;
349: }
350: if (j == c.length) {
351:
352: // all parameters match
353: if (found == true) {
354: throw new MethodProperty.MethodException(
355: "Could not identify unique "
356: + setMethodName + "-method");
357: } else {
358: found = true;
359: setMethod = m[i];
360: }
361: }
362: }
363: if (found != true) {
364: throw new MethodProperty.MethodException(
365: "Could not identify " + setMethodName
366: + "-method");
367: }
368: }
369:
370: // Get the return type from get method
371: if (type.isPrimitive()) {
372: if (type.equals(Boolean.TYPE))
373: type = Boolean.class;
374: else if (type.equals(Integer.TYPE))
375: type = Integer.class;
376: else if (type.equals(Float.TYPE))
377: type = Float.class;
378: else if (type.equals(Double.TYPE))
379: type = Double.class;
380: else if (type.equals(Byte.TYPE))
381: type = Byte.class;
382: else if (type.equals(Character.TYPE))
383: type = Character.class;
384: else if (type.equals(Short.TYPE))
385: type = Short.class;
386: else if (type.equals(Long.TYPE))
387: type = Long.class;
388: }
389:
390: setArguments(getArgs, setArgs, setArgumentIndex);
391: this .readOnly = (setMethod == null);
392: this .instance = instance;
393: }
394:
395: /** <p>Creates a new instance of MethodProperty from the getter and
396: * setter methods, and argument lists. This constructor behaves exctly
397: * like {@link #MethodProperty(Class type, Object instance, String
398: * getMethodName, String setMethodName, Object [] getArgs, Object []
399: * setArgs, int setArgumentIndex)} except that instead of names of
400: * the getter and setter methods this constructor is given the actual
401: * methods themselves.</p>
402: *
403: * @param type type of the property
404: * @param instance object that includes the property
405: * @param getMethod the getter method
406: * @param setMethod the setter method
407: * @param getArgs fixed argument list to be passed to the getter method
408: * @param setArgs fixed argument list to be passed to the setter method
409: * @param setArgumentIndex the index of the argument in
410: * <code>setArgs</code> to be replaced with <code>newValue</code> when
411: * {@link #setValue(Object newValue)} is called
412: */
413: public MethodProperty(Class type, Object instance,
414: Method getMethod, Method setMethod, Object[] getArgs,
415: Object[] setArgs, int setArgumentIndex) {
416:
417: if (getMethod == null) {
418: throw new MethodProperty.MethodException(
419: "Property GET-method cannot not be null: " + type);
420: }
421:
422: if (setMethod != null
423: && (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length))
424: throw new IndexOutOfBoundsException(
425: "The setArgumentIndex must be >= 0 and < setArgs.length");
426:
427: // Get the return type from get method
428: if (type.isPrimitive()) {
429: if (type.equals(Boolean.TYPE))
430: type = Boolean.class;
431: else if (type.equals(Integer.TYPE))
432: type = Integer.class;
433: else if (type.equals(Float.TYPE))
434: type = Float.class;
435: else if (type.equals(Double.TYPE))
436: type = Double.class;
437: else if (type.equals(Byte.TYPE))
438: type = Byte.class;
439: else if (type.equals(Character.TYPE))
440: type = Character.class;
441: else if (type.equals(Short.TYPE))
442: type = Short.class;
443: else if (type.equals(Long.TYPE))
444: type = Long.class;
445: }
446:
447: this .getMethod = getMethod;
448: this .setMethod = setMethod;
449: setArguments(getArgs, setArgs, setArgumentIndex);
450: this .readOnly = (setMethod == null);
451: this .instance = instance;
452: this .type = type;
453: }
454:
455: /** Returns the type of the Property. The methods <code>getValue</code>
456: * and <code>setValue</code> must be compatible with this type: one
457: * must be able to safely cast the value returned from
458: * <code>getValue</code> to the given type and pass any variable
459: * assignable to this type as an argument to <code>setValue</code>.
460: *
461: * @return type of the Property
462: */
463: public final Class getType() {
464: return type;
465: }
466:
467: /** Tests if the object is in read-only mode. In read-only mode calls
468: * to <code>setValue</code> will throw <code>ReadOnlyException</code>s
469: * and will not modify the value of the Property.
470: *
471: * @return <code>true</code> if the object is in read-only mode,
472: * <code>false</code> if it's not
473: */
474: public boolean isReadOnly() {
475: return readOnly;
476: }
477:
478: /** Gets the value stored in the Property. The value is resolved by
479: * calling the specified getter method with the argument specified
480: * at instantiation.
481: *
482: * @return the value of the Property
483: */
484: public Object getValue() {
485: try {
486: return getMethod.invoke(instance, getArgs);
487: } catch (Throwable e) {
488: throw new MethodProperty.MethodException(e);
489: }
490: }
491:
492: /** Returns the value of the MethodProperty in human readable textual
493: * format. The return value should be assignable to the
494: * <code>setValue</code> method if the Property is not in read-only
495: * mode.
496: *
497: * @return String representation of the value stored in the Property
498: */
499: public String toString() {
500: Object value = getValue();
501: if (value == null)
502: return null;
503: return value.toString();
504: }
505:
506: /** <p>Sets the setter method and getter method argument lists.</p>
507: *
508: * @param getArgs fixed argument list to be passed to the getter method
509: * @param setArgs fixed argument list to be passed to the setter method
510: * @param setArgumentIndex the index of the argument in
511: * <code>setArgs</code> to be replaced with <code>newValue</code> when
512: * {@link #setValue(Object newValue)} is called
513: */
514: public void setArguments(Object[] getArgs, Object[] setArgs,
515: int setArgumentIndex) {
516: this .getArgs = new Object[getArgs.length];
517: for (int i = 0; i < getArgs.length; i++)
518: this .getArgs[i] = getArgs[i];
519: this .setArgs = new Object[setArgs.length];
520: for (int i = 0; i < setArgs.length; i++)
521: this .setArgs[i] = setArgs[i];
522: this .setArgumentIndex = setArgumentIndex;
523: }
524:
525: /** Set the value of the property. This method supports setting from
526: * <code>String</code>s if either <code>String</code> is directly
527: * assignable to property type, or the type class contains a string
528: * constructor.
529: *
530: * @param newValue New value of the property.
531: * @throws <code>Property.ReadOnlyException</code> if the object is in
532: * read-only mode
533: * @throws <code>Property.ConversionException</code> if
534: * <code>newValue</code> can't be converted into the Property's native
535: * type directly or through <code>String</code>
536: */
537: public void setValue(Object newValue)
538: throws Property.ReadOnlyException,
539: Property.ConversionException {
540:
541: // Check the mode
542: if (isReadOnly())
543: throw new Property.ReadOnlyException();
544:
545: // Try to assign the compatible value directly
546: if (newValue == null
547: || type.isAssignableFrom(newValue.getClass()))
548: invokeSetMethod(newValue);
549:
550: // Otherwise try to convert the value trough string constructor
551: else {
552:
553: Object value;
554: try {
555:
556: // Get the string constructor
557: Constructor constr = getType().getConstructor(
558: new Class[] { String.class });
559:
560: value = constr.newInstance(new Object[] { newValue
561: .toString() });
562:
563: } catch (java.lang.Exception e) {
564: throw new Property.ConversionException(e);
565: }
566:
567: // Create new object from the string
568: invokeSetMethod(value);
569: }
570: }
571:
572: /** Internal method to actually call the setter method of the wrapped
573: * property.
574: */
575: private void invokeSetMethod(Object value) {
576:
577: try {
578: // Construct a temporary argument array only if needed
579: if (setArgs.length == 1)
580: setMethod.invoke(instance, new Object[] { value });
581: else {
582:
583: // Set the value to argument array
584: Object[] args = new Object[setArgs.length];
585: for (int i = 0; i < setArgs.length; i++)
586: args[i] = (i == setArgumentIndex) ? value
587: : setArgs[i];
588: setMethod.invoke(instance, args);
589: }
590: } catch (InvocationTargetException e) {
591: Throwable targetException = e.getTargetException();
592: throw new MethodProperty.MethodException(targetException);
593: } catch (Exception e) {
594: throw new MethodProperty.MethodException(e);
595: }
596: }
597:
598: /** Sets the Property's read-only mode to the specified status.
599: *
600: * @param newStatus new read-only status of the Property
601: */
602: public void setReadOnly(boolean newStatus) {
603: if (newStatus)
604: readOnly = true;
605: else
606: readOnly = (setMethod == null);
607: }
608:
609: /** <code>Exception</code> object that signals that there were
610: * problems calling or finding the specified getter or setter methods
611: * of the property.
612: * @author IT Mill Ltd.
613: * @version 3.1.1
614: * @since 3.0
615: */
616: public class MethodException extends RuntimeException {
617:
618: /**
619: * Serial generated by eclipse.
620: */
621: private static final long serialVersionUID = 3690473623827855153L;
622: /** Cause of the method exception */
623: private Throwable cause;
624:
625: /** Constructs a new <code>MethodException</code> with the
626: * specified detail message.
627: *
628: * @param msg the detail message
629: */
630: public MethodException(String msg) {
631: super (msg);
632: }
633:
634: /** Constructs a new <code>MethodException</code> from another
635: * exception.
636: *
637: * @param exception cause of the exception
638: */
639: public MethodException(Throwable cause) {
640: this .cause = cause;
641: }
642:
643: /**
644: * @see java.lang.Throwable#getCause()
645: */
646: public Throwable getCause() {
647: return cause;
648: }
649:
650: /** Get the method property this exception originates from */
651: public MethodProperty getMethodProperty() {
652: return MethodProperty.this ;
653: }
654: }
655:
656: /* Events *************************************************************** */
657:
658: /** An <code>Event</code> object specifying the Property whose read-only
659: * status has been changed.
660: * @author IT Mill Ltd.
661: * @version 3.1.1
662: * @since 3.0
663: */
664: private class ReadOnlyStatusChangeEvent extends
665: java.util.EventObject implements
666: Property.ReadOnlyStatusChangeEvent {
667:
668: /**
669: * Serial generated by eclipse.
670: */
671: private static final long serialVersionUID = 3258129163305955896L;
672:
673: /** Constructs a new read-only status change event for this object.
674: *
675: * @param source source object of the event
676: */
677: protected ReadOnlyStatusChangeEvent(MethodProperty source) {
678: super (source);
679: }
680:
681: /** Gets the Property whose read-only state has changed.
682: *
683: * @return source Property of the event.
684: */
685: public Property getProperty() {
686: return (Property) getSource();
687: }
688:
689: }
690:
691: /** Registers a new read-only status change listener for this Property.
692: *
693: * @param listener the new Listener to be registered
694: */
695: public void addListener(
696: Property.ReadOnlyStatusChangeListener listener) {
697: if (readOnlyStatusChangeListeners == null)
698: readOnlyStatusChangeListeners = new LinkedList();
699: readOnlyStatusChangeListeners.add(listener);
700: }
701:
702: /** Remove a previously registered read-only status change listener.
703: *
704: * @param listener listener to be removed
705: */
706: public void removeListener(
707: Property.ReadOnlyStatusChangeListener listener) {
708: if (readOnlyStatusChangeListeners != null)
709: readOnlyStatusChangeListeners.remove(listener);
710: }
711:
712: /** Send a read only status change event to all registered listeners.
713: */
714: private void fireReadOnlyStatusChange() {
715: if (readOnlyStatusChangeListeners != null) {
716: Object[] l = readOnlyStatusChangeListeners.toArray();
717: Property.ReadOnlyStatusChangeEvent event = new MethodProperty.ReadOnlyStatusChangeEvent(
718: this );
719: for (int i = 0; i < l.length; i++)
720: ((Property.ReadOnlyStatusChangeListener) l[i])
721: .readOnlyStatusChange(event);
722: }
723: }
724:
725: }
|