/* @VaadinApache2LicenseForJavaFiles@ */ package com.vaadin.data.util; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.logging.Level; import java.util.logging.Logger; import com.vaadin.data.Property; import com.vaadin.util.SerializerHelper; /** *

* Proxy class for creating Properties from pairs of getter and setter methods * of a Bean property. An instance of this class can be thought as having been * attached to a field of an object. Accessing the object through the Property * interface directly manipulates the underlying field. *

* *

* It's assumed that the return value returned by the getter method is * assignable to the type of the property, and the setter method parameter is * assignable to that value. *

* *

* A valid getter method must always be available, but instance of this class * can be constructed with a null setter method in which case the * resulting MethodProperty is read-only. *

* *

* MethodProperty implements Property.ValueChangeNotifier, but does not * automatically know whether or not the getter method will actually return a * new value - value change listeners are always notified when setValue is * called, without verifying what the getter returns. *

* * @author Vaadin Ltd. * @since 3.0 */ @SuppressWarnings("serial") public class MethodProperty extends AbstractProperty { /** * The object that includes the property the MethodProperty is bound to. */ private transient Object instance; /** * Argument arrays for the getter and setter methods. */ private transient Object[] setArgs, getArgs; /** * The getter and setter methods. */ private transient Method setMethod, getMethod; /** * Index of the new value in the argument list for the setter method. If the * setter method requires several parameters, this index tells which one is * the actual value to change. */ private int setArgumentIndex; /** * Type of the property. */ private transient Class type; /* Special serialization to handle method references */ private void writeObject(java.io.ObjectOutputStream out) throws IOException { out.defaultWriteObject(); SerializerHelper.writeClass(out, type); out.writeObject(instance); out.writeObject(setArgs); out.writeObject(getArgs); if (setMethod != null) { out.writeObject(setMethod.getName()); SerializerHelper .writeClassArray(out, setMethod.getParameterTypes()); } else { out.writeObject(null); out.writeObject(null); } if (getMethod != null) { out.writeObject(getMethod.getName()); SerializerHelper .writeClassArray(out, getMethod.getParameterTypes()); } else { out.writeObject(null); out.writeObject(null); } }; /* Special serialization to handle method references */ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); try { @SuppressWarnings("unchecked") // business assumption; type parameters not checked at runtime Class class1 = (Class) SerializerHelper.readClass(in); type = class1; instance = in.readObject(); setArgs = (Object[]) in.readObject(); getArgs = (Object[]) in.readObject(); String name = (String) in.readObject(); Class[] paramTypes = SerializerHelper.readClassArray(in); if (name != null) { setMethod = instance.getClass().getMethod(name, paramTypes); } else { setMethod = null; } name = (String) in.readObject(); paramTypes = SerializerHelper.readClassArray(in); if (name != null) { getMethod = instance.getClass().getMethod(name, paramTypes); } else { getMethod = null; } } catch (SecurityException e) { getLogger().log(Level.SEVERE, "Internal deserialization error", e); } catch (NoSuchMethodException e) { getLogger().log(Level.SEVERE, "Internal deserialization error", e); } }; /** *

* Creates a new instance of MethodProperty from a named bean * property. This constructor takes an object and the name of a bean * property and initializes itself with the accessor methods for the * property. *

*

* The getter method of a MethodProperty instantiated with this * constructor will be called with no arguments, and the setter method with * only the new value as the sole argument. *

* *

* If the setter method is unavailable, the resulting * MethodProperty will be read-only, otherwise it will be * read-write. *

* *

* Method names are constructed from the bean property by adding * get/is/are/set prefix and capitalising the first character in the name of * the given bean property. *

* * @param instance * the object that includes the property. * @param beanPropertyName * the name of the property to bind to. */ @SuppressWarnings("unchecked") public MethodProperty(Object instance, String beanPropertyName) { final Class beanClass = instance.getClass(); // Assure that the first letter is upper cased (it is a common // mistake to write firstName, not FirstName). if (Character.isLowerCase(beanPropertyName.charAt(0))) { final char[] buf = beanPropertyName.toCharArray(); buf[0] = Character.toUpperCase(buf[0]); beanPropertyName = new String(buf); } // Find the get method getMethod = null; try { getMethod = initGetterMethod(beanPropertyName, beanClass); } catch (final java.lang.NoSuchMethodException ignored) { throw new MethodException(this, "Bean property " + beanPropertyName + " can not be found"); } // In case the get method is found, resolve the type Class returnType = getMethod.getReturnType(); // Finds the set method setMethod = null; try { setMethod = beanClass.getMethod("set" + beanPropertyName, new Class[] { returnType }); } catch (final java.lang.NoSuchMethodException skipped) { } // Gets the return type from get method if (returnType.isPrimitive()) { type = (Class) convertPrimitiveType(returnType); if (type.isPrimitive()) { throw new MethodException(this, "Bean property " + beanPropertyName + " getter return type must not be void"); } } else { type = (Class) returnType; } setArguments(new Object[] {}, new Object[] { null }, 0); this.instance = instance; } /** *

* Creates a new instance of MethodProperty from named getter * and setter methods. The getter method of a MethodProperty * instantiated with this constructor will be called with no arguments, and * the setter method with only the new value as the sole argument. *

* *

* If the setter method is null, the resulting * MethodProperty will be read-only, otherwise it will be * read-write. *

* * @param type * the type of the property. * @param instance * the object that includes the property. * @param getMethodName * the name of the getter method. * @param setMethodName * the name of the setter method. * */ public MethodProperty(Class type, Object instance, String getMethodName, String setMethodName) { this(type, instance, getMethodName, setMethodName, new Object[] {}, new Object[] { null }, 0); } /** *

* Creates a new instance of MethodProperty with the getter and * setter methods. The getter method of a MethodProperty * instantiated with this constructor will be called with no arguments, and * the setter method with only the new value as the sole argument. *

* *

* If the setter method is null, the resulting * MethodProperty will be read-only, otherwise it will be * read-write. *

* * @param type * the type of the property. * @param instance * the object that includes the property. * @param getMethod * the getter method. * @param setMethod * the setter method. */ public MethodProperty(Class type, Object instance, Method getMethod, Method setMethod) { this(type, instance, getMethod, setMethod, new Object[] {}, new Object[] { null }, 0); } /** *

* Creates a new instance of MethodProperty from named getter * and setter methods and argument lists. The getter method of a * MethodProperty instantiated with this constructor will be * called with the getArgs as arguments. The setArgs will be used as the * arguments for the setter method, though the argument indexed by the * setArgumentIndex will be replaced with the argument passed to the * {@link #setValue(Object newValue)} method. *

* *

* For example, if the setArgs contains A, * B and C, and setArgumentIndex = * 1, the call methodProperty.setValue(X) would result * in the setter method to be called with the parameter set of * {A, X, C} *

* * @param type * the type of the property. * @param instance * the object that includes the property. * @param getMethodName * the name of the getter method. * @param setMethodName * the name of the setter method. * @param getArgs * the fixed argument list to be passed to the getter method. * @param setArgs * the fixed argument list to be passed to the setter method. * @param setArgumentIndex * the index of the argument in setArgs to be * replaced with newValue when * {@link #setValue(Object newValue)} is called. */ @SuppressWarnings("unchecked") public MethodProperty(Class type, Object instance, String getMethodName, String setMethodName, Object[] getArgs, Object[] setArgs, int setArgumentIndex) { // Check the setargs and setargs index if (setMethodName != null && setArgs == null) { throw new IndexOutOfBoundsException("The setArgs can not be null"); } if (setMethodName != null && (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length)) { throw new IndexOutOfBoundsException( "The setArgumentIndex must be >= 0 and < setArgs.length"); } // Set type this.type = type; // Find set and get -methods final Method[] m = instance.getClass().getMethods(); // Finds get method boolean found = false; for (int i = 0; i < m.length; i++) { // Tests the name of the get Method if (!m[i].getName().equals(getMethodName)) { // name does not match, try next method continue; } // Tests return type if (!type.equals(m[i].getReturnType())) { continue; } // Tests the parameter types final Class[] c = m[i].getParameterTypes(); if (c.length != getArgs.length) { // not the right amount of parameters, try next method continue; } int j = 0; while (j < c.length) { if (getArgs[j] != null && !c[j].isAssignableFrom(getArgs[j].getClass())) { // parameter type does not match, try next method break; } j++; } if (j == c.length) { // all paramteters matched if (found == true) { throw new MethodException(this, "Could not uniquely identify " + getMethodName + "-method"); } else { found = true; getMethod = m[i]; } } } if (found != true) { throw new MethodException(this, "Could not find " + getMethodName + "-method"); } // Finds set method if (setMethodName != null) { // Finds setMethod found = false; for (int i = 0; i < m.length; i++) { // Checks name if (!m[i].getName().equals(setMethodName)) { // name does not match, try next method continue; } // Checks parameter compatibility final Class[] c = m[i].getParameterTypes(); if (c.length != setArgs.length) { // not the right amount of parameters, try next method continue; } int j = 0; while (j < c.length) { if (setArgs[j] != null && !c[j].isAssignableFrom(setArgs[j].getClass())) { // parameter type does not match, try next method break; } else if (j == setArgumentIndex && !c[j].equals(type)) { // Property type is not the same as setArg type break; } j++; } if (j == c.length) { // all parameters match if (found == true) { throw new MethodException(this, "Could not identify unique " + setMethodName + "-method"); } else { found = true; setMethod = m[i]; } } } if (found != true) { throw new MethodException(this, "Could not identify " + setMethodName + "-method"); } } // Gets the return type from get method this.type = (Class) convertPrimitiveType(type); setArguments(getArgs, setArgs, setArgumentIndex); this.instance = instance; } /** *

* Creates a new instance of MethodProperty from the getter and * setter methods, and argument lists. *

*

* This constructor behaves exactly like * {@link #MethodProperty(Class type, Object instance, String getMethodName, String setMethodName, Object [] getArgs, Object [] setArgs, int setArgumentIndex)} * except that instead of names of the getter and setter methods this * constructor is given the actual methods themselves. *

* * @param type * the type of the property. * @param instance * the object that includes the property. * @param getMethod * the getter method. * @param setMethod * the setter method. * @param getArgs * the fixed argument list to be passed to the getter method. * @param setArgs * the fixed argument list to be passed to the setter method. * @param setArgumentIndex * the index of the argument in setArgs to be * replaced with newValue when * {@link #setValue(Object newValue)} is called. */ @SuppressWarnings("unchecked") // cannot use "Class" because of automatic primitive type // conversions public MethodProperty(Class type, Object instance, Method getMethod, Method setMethod, Object[] getArgs, Object[] setArgs, int setArgumentIndex) { if (getMethod == null) { throw new MethodException(this, "Property GET-method cannot not be null: " + type); } if (setMethod != null) { if (setArgs == null) { throw new IndexOutOfBoundsException( "The setArgs can not be null"); } if (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length) { throw new IndexOutOfBoundsException( "The setArgumentIndex must be >= 0 and < setArgs.length"); } } // Gets the return type from get method Class convertedType = (Class) convertPrimitiveType(type); this.getMethod = getMethod; this.setMethod = setMethod; setArguments(getArgs, setArgs, setArgumentIndex); this.instance = instance; this.type = convertedType; } /** * Find a getter method for a property (getXyz(), isXyz() or areXyz()). * * @param propertyName * name of the property * @param beanClass * class in which to look for the getter methods * @return Method * @throws NoSuchMethodException * if no getter found */ static Method initGetterMethod(String propertyName, final Class beanClass) throws NoSuchMethodException { propertyName = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); Method getMethod = null; try { getMethod = beanClass.getMethod("get" + propertyName, new Class[] {}); } catch (final java.lang.NoSuchMethodException ignored) { try { getMethod = beanClass.getMethod("is" + propertyName, new Class[] {}); } catch (final java.lang.NoSuchMethodException ignoredAsWell) { getMethod = beanClass.getMethod("are" + propertyName, new Class[] {}); } } return getMethod; } static Class convertPrimitiveType(Class type) { // Gets the return type from get method if (type.isPrimitive()) { if (type.equals(Boolean.TYPE)) { type = Boolean.class; } else if (type.equals(Integer.TYPE)) { type = Integer.class; } else if (type.equals(Float.TYPE)) { type = Float.class; } else if (type.equals(Double.TYPE)) { type = Double.class; } else if (type.equals(Byte.TYPE)) { type = Byte.class; } else if (type.equals(Character.TYPE)) { type = Character.class; } else if (type.equals(Short.TYPE)) { type = Short.class; } else if (type.equals(Long.TYPE)) { type = Long.class; } } return type; } /** * Returns the type of the Property. The methods getValue and * setValue must be compatible with this type: one must be able * to safely cast the value returned from getValue to the given * type and pass any variable assignable to this type as an argument to * setValue. * * @return type of the Property */ @Override public final Class getType() { return type; } /** * Tests if the object is in read-only mode. In read-only mode calls to * setValue will throw ReadOnlyException and will * not modify the value of the Property. * * @return true if the object is in read-only mode, * false if it's not */ @Override public boolean isReadOnly() { return super.isReadOnly() || (setMethod == null); } /** * Gets the value stored in the Property. The value is resolved by calling * the specified getter method with the argument specified at instantiation. * * @return the value of the Property */ @Override public T getValue() { try { return (T) getMethod.invoke(instance, getArgs); } catch (final Throwable e) { throw new MethodException(this, e); } } /** *

* Sets the setter method and getter method argument lists. *

* * @param getArgs * the fixed argument list to be passed to the getter method. * @param setArgs * the fixed argument list to be passed to the setter method. * @param setArgumentIndex * the index of the argument in setArgs to be * replaced with newValue when * {@link #setValue(Object newValue)} is called. */ public void setArguments(Object[] getArgs, Object[] setArgs, int setArgumentIndex) { this.getArgs = new Object[getArgs.length]; for (int i = 0; i < getArgs.length; i++) { this.getArgs[i] = getArgs[i]; } this.setArgs = new Object[setArgs.length]; for (int i = 0; i < setArgs.length; i++) { this.setArgs[i] = setArgs[i]; } this.setArgumentIndex = setArgumentIndex; } /** * Sets the value of the property. * * Note that since Vaadin 7, no conversions are performed and the value must * be of the correct type. * * @param newValue * the New value of the property. * @throws Property.ReadOnlyException if the object is in * read-only mode. * @see #invokeSetMethod(Object) */ @Override @SuppressWarnings("unchecked") public void setValue(Object newValue) throws Property.ReadOnlyException { // Checks the mode if (isReadOnly()) { throw new Property.ReadOnlyException(); } // Checks the type of the value if (newValue != null && !type.isAssignableFrom(newValue.getClass())) { throw new IllegalArgumentException( "Invalid value type for ObjectProperty."); } invokeSetMethod((T) newValue); fireValueChange(); } /** * Internal method to actually call the setter method of the wrapped * property. * * @param value */ protected void invokeSetMethod(T value) { try { // Construct a temporary argument array only if needed if (setArgs.length == 1) { setMethod.invoke(instance, new Object[] { value }); } else { // Sets the value to argument array final Object[] args = new Object[setArgs.length]; for (int i = 0; i < setArgs.length; i++) { args[i] = (i == setArgumentIndex) ? value : setArgs[i]; } setMethod.invoke(instance, args); } } catch (final InvocationTargetException e) { final Throwable targetException = e.getTargetException(); throw new MethodException(this, targetException); } catch (final Exception e) { throw new MethodException(this, e); } } /** * Exception object that signals that there were problems * calling or finding the specified getter or setter methods of the * property. * * @author Vaadin Ltd. * @since 3.0 */ @SuppressWarnings("rawtypes") // Exceptions cannot be parameterized, ever. public static class MethodException extends RuntimeException { /** * The method property from which the exception originates from */ private final Property property; /** * Cause of the method exception */ private Throwable cause; /** * Constructs a new MethodException with the specified * detail message. * * @param property * the property. * @param msg * the detail message. */ public MethodException(Property property, String msg) { super(msg); this.property = property; } /** * Constructs a new MethodException from another exception. * * @param property * the property. * @param cause * the cause of the exception. */ public MethodException(Property property, Throwable cause) { this.property = property; this.cause = cause; } /** * @see java.lang.Throwable#getCause() */ @Override public Throwable getCause() { return cause; } /** * Gets the method property this exception originates from. * * @return MethodProperty or null if not a valid MethodProperty */ public MethodProperty getMethodProperty() { return (property instanceof MethodProperty) ? (MethodProperty) property : null; } /** * Gets the method property this exception originates from. * * @return Property from which the exception originates */ public Property getProperty() { return property; } } /** * Sends a value change event to all registered listeners. * * Public for backwards compatibility, visibility may be reduced in future * versions. */ @Override public void fireValueChange() { super.fireValueChange(); } private static final Logger getLogger() { return Logger.getLogger(MethodProperty.class.getName()); } }