summaryrefslogtreecommitdiffstats
path: root/server/src/com/vaadin/data/util/MethodProperty.java
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/com/vaadin/data/util/MethodProperty.java')
-rw-r--r--server/src/com/vaadin/data/util/MethodProperty.java784
1 files changed, 784 insertions, 0 deletions
diff --git a/server/src/com/vaadin/data/util/MethodProperty.java b/server/src/com/vaadin/data/util/MethodProperty.java
new file mode 100644
index 0000000000..0c64d90481
--- /dev/null
+++ b/server/src/com/vaadin/data/util/MethodProperty.java
@@ -0,0 +1,784 @@
+/*
+@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;
+
+/**
+ * <p>
+ * 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.
+ * </p>
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * <p>
+ * A valid getter method must always be available, but instance of this class
+ * can be constructed with a <code>null</code> setter method in which case the
+ * resulting MethodProperty is read-only.
+ * </p>
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class MethodProperty<T> extends AbstractProperty<T> {
+
+ /**
+ * 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<? extends T> 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<T> class1 = (Class<T>) 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);
+ }
+ };
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> 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.
+ * </p>
+ * <p>
+ * The getter method of a <code>MethodProperty</code> instantiated with this
+ * constructor will be called with no arguments, and the setter method with
+ * only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is unavailable, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @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<T>) convertPrimitiveType(returnType);
+ if (type.isPrimitive()) {
+ throw new MethodException(this, "Bean property "
+ + beanPropertyName
+ + " getter return type must not be void");
+ }
+ } else {
+ type = (Class<T>) returnType;
+ }
+
+ setArguments(new Object[] {}, new Object[] { null }, 0);
+ this.instance = instance;
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from named getter
+ * and setter methods. The getter method of a <code>MethodProperty</code>
+ * instantiated with this constructor will be called with no arguments, and
+ * the setter method with only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is <code>null</code>, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * @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<? extends T> type, Object instance,
+ String getMethodName, String setMethodName) {
+ this(type, instance, getMethodName, setMethodName, new Object[] {},
+ new Object[] { null }, 0);
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> with the getter and
+ * setter methods. The getter method of a <code>MethodProperty</code>
+ * instantiated with this constructor will be called with no arguments, and
+ * the setter method with only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is <code>null</code>, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * @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<? extends T> type, Object instance,
+ Method getMethod, Method setMethod) {
+ this(type, instance, getMethod, setMethod, new Object[] {},
+ new Object[] { null }, 0);
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from named getter
+ * and setter methods and argument lists. The getter method of a
+ * <code>MethodProperty</code> 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.
+ * </p>
+ *
+ * <p>
+ * For example, if the <code>setArgs</code> contains <code>A</code>,
+ * <code>B</code> and <code>C</code>, and <code>setArgumentIndex =
+ * 1</code>, the call <code>methodProperty.setValue(X)</code> would result
+ * in the setter method to be called with the parameter set of
+ * <code>{A, X, C}</code>
+ * </p>
+ *
+ * @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 <code>setArgs</code> to be
+ * replaced with <code>newValue</code> when
+ * {@link #setValue(Object newValue)} is called.
+ */
+ @SuppressWarnings("unchecked")
+ public MethodProperty(Class<? extends T> 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<T>) convertPrimitiveType(type);
+
+ setArguments(getArgs, setArgs, setArgumentIndex);
+ this.instance = instance;
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from the getter and
+ * setter methods, and argument lists.
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @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 <code>setArgs</code> to be
+ * replaced with <code>newValue</code> when
+ * {@link #setValue(Object newValue)} is called.
+ */
+ @SuppressWarnings("unchecked")
+ // cannot use "Class<? extends T>" 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<? extends T> convertedType = (Class<? extends T>) 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 <code>getValue</code> and
+ * <code>setValue</code> must be compatible with this type: one must be able
+ * to safely cast the value returned from <code>getValue</code> to the given
+ * type and pass any variable assignable to this type as an argument to
+ * <code>setValue</code>.
+ *
+ * @return type of the Property
+ */
+ @Override
+ public final Class<? extends T> getType() {
+ return type;
+ }
+
+ /**
+ * Tests if the object is in read-only mode. In read-only mode calls to
+ * <code>setValue</code> will throw <code>ReadOnlyException</code> and will
+ * not modify the value of the Property.
+ *
+ * @return <code>true</code> if the object is in read-only mode,
+ * <code>false</code> 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);
+ }
+ }
+
+ /**
+ * <p>
+ * Sets the setter method and getter method argument lists.
+ * </p>
+ *
+ * @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 <code>setArgs</code> to be
+ * replaced with <code>newValue</code> 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 <code>Property.ReadOnlyException</code> 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);
+ }
+ }
+
+ /**
+ * <code>Exception</code> object that signals that there were problems
+ * calling or finding the specified getter or setter methods of the
+ * property.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @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 <code>MethodException</code> 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 <code>MethodException</code> 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());
+ }
+}