001: package net.sf.jmoney.model2;
002:
003: import java.lang.reflect.InvocationTargetException;
004: import java.lang.reflect.Method;
005: import java.util.Comparator;
006:
007: import org.eclipse.swt.widgets.Composite;
008:
009: public class ScalarPropertyAccessor<V> extends PropertyAccessor {
010:
011: private int weight;
012:
013: private int minimumWidth;
014:
015: private boolean sortable;
016:
017: /**
018: * never null
019: */
020: private IPropertyControlFactory<V> propertyControlFactory;
021:
022: /**
023: * never null
024: */
025: private Method theGetMethod;
026:
027: /**
028: * never null
029: */
030: private Method theSetMethod;
031:
032: /**
033: * The class of the property values (if the method signatures show int,
034: * long, boolean, or char, this field will be Integer.class, Long.class,
035: * Boolean.class, or Character.class)
036: */
037: private Class<V> classOfValueObject;
038:
039: /**
040: * The class of the property values (if the method signatures show int,
041: * long, boolean, or char, this field will be int.class, long.class,
042: * boolean.class, or char.class)
043: */
044: private Class<?> classOfValueType;
045:
046: private boolean nullAllowed;
047:
048: /**
049: * Index into the array of scalar properties.
050: */
051: private int indexIntoScalarProperties = -1;
052:
053: private Comparator<ExtendableObject> parentComparator;
054:
055: /*
056: * Helper class to tie types together in a type safe manner.
057: */
058: // TODO: If we added the type of the containing object as a type parameter
059: // of this ScalarPropertyAccessor class then this class would no longer
060: // be necessary.
061: private class TypesafePropertyDependency<E> {
062: Class<E> classOfContainingObject;
063: IPropertyDependency<E> dependency;
064:
065: TypesafePropertyDependency(Class<E> classOfContainingObject,
066: IPropertyDependency<E> dependency) {
067: this .classOfContainingObject = classOfContainingObject;
068: this .dependency = dependency;
069: }
070:
071: public boolean isApplicable(ExtendableObject containingObject) {
072: return dependency.isApplicable(classOfContainingObject
073: .cast(containingObject));
074: }
075: }
076:
077: private TypesafePropertyDependency<?> typesafeDependency;
078:
079: public <E> ScalarPropertyAccessor(Class<V> classOfValueObject,
080: PropertySet<E> propertySet, String localName,
081: String displayName, int weight, int minimumWidth,
082: final IPropertyControlFactory<V> propertyControlFactory,
083: IPropertyDependency<E> propertyDependency) {
084: super (propertySet, localName, displayName);
085:
086: this .weight = weight;
087: this .minimumWidth = minimumWidth;
088: this .sortable = true;
089: this .propertyControlFactory = propertyControlFactory;
090:
091: this .typesafeDependency = (propertyDependency == null) ? null
092: : new TypesafePropertyDependency<E>(propertySet
093: .getImplementationClass(), propertyDependency);
094:
095: Class implementationClass = propertySet
096: .getImplementationClass();
097:
098: if (classOfValueObject.isPrimitive()) {
099: throw new MalformedPluginException(
100: "Property '"
101: + localName
102: + "' in '"
103: + implementationClass.getName()
104: + "' has been parameterized by a primitive type ("
105: + classOfValueObject.getName()
106: + "). Although primitive types may be used by the getters and setters, the equivalent object classes must be used for parameterization.");
107: }
108:
109: // Use introspection on the interface to find the getter method.
110: // Following the Java beans pattern, we allow the getter for a
111: // boolean property to have a prefix of either "get" or "is".
112: try {
113: theGetMethod = findMethod("get", localName, null);
114: } catch (MalformedPluginException e) {
115: try {
116: theGetMethod = findMethod("is", localName, null);
117: if (theGetMethod.getReturnType() != boolean.class) {
118: throw new MalformedPluginException("Method '"
119: + theGetMethod.getName() + "' in '"
120: + implementationClass.getName()
121: + "' must return boolean.");
122: }
123: } catch (MalformedPluginException e2) {
124: String propertyNamePart = localName.toUpperCase()
125: .substring(0, 1)
126: + localName.substring(1, localName.length());
127: throw new MalformedPluginException(
128: "The "
129: + propertySet.getImplementationClass()
130: .getName()
131: + " class must have a method with a signature of get"
132: + propertyNamePart
133: + "() or, if a boolean property, a signature of is"
134: + propertyNamePart + "().");
135: }
136: }
137:
138: if (theGetMethod.getReturnType() == void.class) {
139: throw new MalformedPluginException("Method '"
140: + theGetMethod.getName() + "' in '"
141: + implementationClass.getName()
142: + "' must not return void.");
143: }
144:
145: classOfValueType = theGetMethod.getReturnType();
146:
147: Class<?> classOfObjectReturnedByGetter;
148: if (classOfValueType == int.class) {
149: classOfObjectReturnedByGetter = Integer.class;
150: nullAllowed = false;
151: } else if (classOfValueType == long.class) {
152: classOfObjectReturnedByGetter = Long.class;
153: nullAllowed = false;
154: } else if (classOfValueType == boolean.class) {
155: classOfObjectReturnedByGetter = Boolean.class;
156: nullAllowed = false;
157: } else if (classOfValueType == char.class) {
158: classOfObjectReturnedByGetter = Character.class;
159: nullAllowed = false;
160: } else {
161: classOfObjectReturnedByGetter = classOfValueType;
162: nullAllowed = true;
163: }
164:
165: if (classOfObjectReturnedByGetter != classOfValueObject) {
166: throw new MalformedPluginException(
167: "Method '"
168: + theGetMethod.getName()
169: + "' in '"
170: + implementationClass.getName()
171: + "' returns type '"
172: + theGetMethod.getReturnType().getName()
173: + "' but code metadata indicates the type should be '"
174: + classOfValueObject.getName() + "'.");
175: }
176:
177: this .classOfValueObject = classOfValueObject;
178:
179: // Use introspection on the implementation class to find the setter method.
180: Class parameterTypes[] = { classOfValueType };
181: theSetMethod = findMethod("set", localName, parameterTypes);
182:
183: if (theSetMethod.getReturnType() != void.class) {
184: throw new MalformedPluginException("Method '"
185: + theSetMethod.getName() + "' in '"
186: + implementationClass.getName()
187: + "' must return void type .");
188: }
189:
190: /*
191: * Set the comparator, if any.
192: *
193: * If this object has been given a comparator to compare the values of
194: * the properties, then return a comparator that compares the parent
195: * objects by getting the property value from each parent and comparing
196: * those values. Otherwise return null to indicate that there is no
197: * ordering.
198: */
199: if (propertyControlFactory.getComparator() != null) {
200: parentComparator = new Comparator<ExtendableObject>() {
201: public int compare(ExtendableObject object1,
202: ExtendableObject object2) {
203: V value1 = object1
204: .getPropertyValue(ScalarPropertyAccessor.this );
205: V value2 = object2
206: .getPropertyValue(ScalarPropertyAccessor.this );
207: if (value1 == null && value2 == null)
208: return 0;
209: if (value1 == null)
210: return 1;
211: if (value2 == null)
212: return -1;
213: return propertyControlFactory.getComparator()
214: .compare(value1, value2);
215: }
216: };
217: } else {
218: parentComparator = null;
219: }
220: }
221:
222: /**
223: * The width weighting to be used when this property is displayed
224: * in a table or a grid. If the width available for the table
225: * or grid is more than the sum of the minimum widths then the
226: * excess width is distributed across the columns.
227: * This weight indicates how much the property
228: * can benefit from being given excess width. For example
229: * a property containing a description can benefit, whereas
230: * a property containing a date cannot.
231: */
232: public int getWeight() {
233: return weight;
234: }
235:
236: /**
237: * The minimum width to be used when this property is displayed
238: * in a table or a grid.
239: */
240: public int getMinimumWidth() {
241: return minimumWidth;
242: }
243:
244: /**
245: * Indicates whether users are able to sort views based on this property.
246: */
247: public boolean isSortable() {
248: return sortable;
249: }
250:
251: /**
252: * Indicates whether the property may take null values.
253: *
254: * All properties may take null values except properties whose values are
255: * the intrinsic types (int, long, boolean, char). Properties of type Integer,
256: * Long, Boolean, and Character may take null values.
257: */
258: public boolean isNullAllowed() {
259: return nullAllowed;
260: }
261:
262: /**
263: * The default value for a property is suitable for uses such
264: * as:
265: *
266: * - setting the default columnn value in a database
267: * - providing values when the value is missing from an
268: * XML file
269: *
270: * It is expected that this value is constant (the same value
271: * is always returned for a given property). The results will
272: * be unpredicable if this is not the case.
273: *
274: * @return the default value to use for this property, which may
275: * be null if the property is of a nullable type
276: */
277: public V getDefaultValue() {
278: return propertyControlFactory.getDefaultValue();
279: }
280:
281: /**
282: * Indicates whether the property may be edited by the user.
283: */
284: public boolean isEditable() {
285: return (propertyControlFactory.isEditable());
286: }
287:
288: /**
289: * Return a comparator to be used to compare two extendable objects based on the value
290: * of this property. This method looks to the comparator, if any, provided by
291: * the IPropertyControlFactory implementation. The ordering is thus defined by the
292: * plug-in that added this property.
293: *
294: * @return a comparator, or null if no comparator was provided
295: * for use with this property
296: */
297: public Comparator<ExtendableObject> getComparator() {
298: return parentComparator;
299: }
300:
301: public Method findMethod(String prefix, String propertyName,
302: Class[] parameters) {
303: String methodName = prefix
304: + propertyName.toUpperCase().charAt(0)
305: + propertyName.substring(1, propertyName.length());
306:
307: try {
308: return getDeclaredMethodRecursively(propertySet
309: .getImplementationClass(), methodName, parameters);
310: } catch (NoSuchMethodException e) {
311: String parameterText = "";
312: if (parameters != null) {
313: for (int paramIndex = 0; paramIndex < parameters.length; paramIndex++) {
314: if (paramIndex > 0) {
315: parameterText = parameterText + ", ";
316: }
317: String className = parameters[paramIndex].getName();
318: if (parameters[paramIndex].isArray()) {
319: // The returned class name seems to be a mess when the class is an array,
320: // so we tidy it up.
321: parameterText = parameterText
322: + className.substring(2, className
323: .length() - 1) + "[]";
324: } else {
325: parameterText = parameterText + className;
326: }
327: }
328: }
329: throw new MalformedPluginException("The "
330: + propertySet.getImplementationClass().getName()
331: + " class must have a method with a signature of "
332: + methodName + "(" + parameterText + ").");
333: }
334: }
335:
336: /**
337: * Gets a method from an interface.
338: * Whereas Class.getDeclaredMethod finds a method from an
339: * interface, it will not find the method if the method is
340: * defined in an interface which the given interface extends.
341: * This method will find the method if any of the interfaces
342: * extended by this interface define the method.
343: */
344: private Method getDeclaredMethodRecursively(
345: Class implementationClass, String methodName,
346: Class[] arguments) throws NoSuchMethodException {
347: Class classToTry = implementationClass;
348: do {
349: try {
350: return classToTry.getDeclaredMethod(methodName,
351: arguments);
352: } catch (NoSuchMethodException e) {
353: classToTry = classToTry.getSuperclass();
354: }
355: } while (classToTry != null);
356:
357: throw new NoSuchMethodException();
358: }
359:
360: /**
361: */
362: public V invokeGetMethod(Object invocationTarget) {
363: try {
364: Object value = theGetMethod.invoke(invocationTarget,
365: (Object[]) null);
366: return classOfValueObject.cast(value);
367: } catch (InvocationTargetException e) {
368: // TODO Process this properly
369: e.printStackTrace();
370: throw new RuntimeException("Plugin error");
371: } catch (Exception e) {
372: // IllegalAccessException and IllegalArgumentException exceptions should
373: // not be possible here because the method was checked
374: // for correct access rights and parameters during initialization.
375: // Therefore throw a runtime exception.
376: e.printStackTrace();
377: throw new RuntimeException("internal error");
378: }
379: }
380:
381: public void invokeSetMethod(Object invocationTarget, V value) {
382: try {
383: Object parameters[] = new Object[] { value };
384: theSetMethod.invoke(invocationTarget, parameters);
385: } catch (InvocationTargetException e) {
386: // TODO Process this properly
387: e.getCause().printStackTrace();
388: throw new RuntimeException("Plugin error");
389: } catch (Exception e) {
390: // IllegalAccessException and IllegalArgumentException exceptions should
391: // not be possible here because the method was checked
392: // for correct access rights and parameters during initialization.
393: // Therefore throw a runtime exception.
394: e.printStackTrace();
395: throw new RuntimeException("internal error");
396: }
397: }
398:
399: /**
400: * Create a Control object that edits the property.
401: *
402: * @param parent
403: * @return An interface to a wrapper class.
404: */
405: public IPropertyControl createPropertyControl(Composite parent) {
406: // When a PropertyAccessor object is created, it is
407: // provided with an interface to a factory that constructs
408: // control objects that edit the property.
409: // We call into that factory to create an edit control.
410: return propertyControlFactory.createPropertyControl(parent,
411: this );
412: }
413:
414: /**
415: * Format the value of a property so it can be embedded into a
416: * message.
417: *
418: * The returned value will look sensible when embedded in a message.
419: * Therefore null values and empty values are returned as non-empty
420: * text such as "none" or "empty". Text values are placed in
421: * quotes unless sure that only a single word will be returned that
422: * would be readable without quotes.
423: *
424: * @return The value of the property formatted as appropriate.
425: */
426: public String formatValueForMessage(ExtendableObject object) {
427: // When a PropertyAccessor object is created, it is
428: // provided with an interface to a factory that constructs
429: // control objects that edit the property.
430: // This factory can also format values for us.
431: // We call into that factory to obtain the property value
432: // and format it.
433:
434: // If null or the empty string is returned to us,
435: // change to "empty".
436: String formattedValue = propertyControlFactory
437: .formatValueForMessage(object, this );
438: return (formattedValue == null || formattedValue.length() == 0) ? "empty"
439: : formattedValue;
440: }
441:
442: /**
443: * Format the value of a property as appropriate for displaying in a
444: * table.
445: *
446: * The returned value is expected to be displayed in a table or some similar
447: * view. Null and empty values are therefore returned as empty strings.
448: * Text values are not quoted.
449: *
450: * @return The value of the property formatted as appropriate.
451: */
452: public String formatValueForTable(ExtendableObject object) {
453: // When a PropertyAccessor object is created, it is
454: // provided with an interface to a factory that constructs
455: // control objects that edit the property.
456: // This factory can also format values for us.
457: // We call into that factory to obtain the property value
458: // and format it.
459:
460: // If null is returned to us, change to the empty string.
461: String formattedValue = propertyControlFactory
462: .formatValueForTable(object, this );
463: return (formattedValue == null) ? "" : formattedValue;
464: }
465:
466: /**
467: * Indicates if the property is a list of objects.
468: */
469: @Override
470: public boolean isList() {
471: return false;
472: }
473:
474: /**
475: * Returns the class for the values of this property. This is
476: * usually the class that is returned by the getter method, but if
477: * the getter method returns int, long, boolean, or char then this
478: * method will return Integer.class, Long.class, Boolean.class, or
479: * Character.class.
480: *
481: * @return the class of the property values (if the method signatures show int,
482: * long, boolean, or char, this field will be Integer.class, Long.class,
483: * Boolean.class, or Character.class)
484: */
485: public Class<V> getClassOfValueObject() {
486: return classOfValueObject;
487: }
488:
489: /**
490: * Returns the class for the values of this property. This is the
491: * class that is returned by the getter method.
492: *
493: * @return the class of the property values (if the method signatures show int,
494: * long, boolean, or char, this field will be int.class, long.class,
495: * boolean.class, or char.class)
496: */
497: public Class<?> getClassOfValueType() {
498: return classOfValueType;
499: }
500:
501: /**
502: * It is often useful to have an array of property values
503: * of an extendable object. This array contains all scalar
504: * properties in the extendable object, including extension
505: * properties and properties from any base property sets.
506: * <P>
507: * In these arrays, the properties (including extension properties)
508: * from the base property sets are put first in the array.
509: * This means a given property will always be at the same index
510: * in the array regardless of the actual derived property set.
511: * <P>
512: * This index is guaranteed to match the order in which
513: * properties are returned by the PropertySet.getPropertyIterator_Scalar3().
514: * i.e. if this method returns n then in every case where the
515: * collection returned by getPropertyIterator_Scalar3 contains this property,
516: * this property will be returned as the (n+1)'th element in the collection.
517: *
518: * @return the index of this property in the list of scalar
519: * properties for the class. This method returns zero
520: * for the first scalar property returned by
521: * PropertySet.getPropertyIterator3() and so on.
522: */
523: public int getIndexIntoScalarProperties() {
524: return indexIntoScalarProperties;
525: }
526:
527: // TODO: This method should be accessible only from within the package.
528: public void setIndexIntoScalarProperties(
529: int indexIntoScalarProperties) {
530: this .indexIntoScalarProperties = indexIntoScalarProperties;
531: }
532:
533: /**
534: * Indicates if this property is applicable. An instance of an object
535: * containing this property is passed. This method should look to the values
536: * of the other properties in the object to determine if this property is applicable.
537: *
538: * If the property is not applicable then the UI should not show a value for this
539: * property nor allow it to be updated.
540: *
541: * @return
542: */
543: public boolean isPropertyApplicable(
544: ExtendableObject containingObject) {
545: if (typesafeDependency == null) {
546: return true;
547: } else {
548: return typesafeDependency.isApplicable(containingObject);
549: }
550: }
551: }
|