/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.v7.data.util; import static com.vaadin.util.ReflectTools.convertPrimitiveType; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; import com.vaadin.data.Binder; import com.vaadin.data.ValueProvider; import com.vaadin.server.Setter; import com.vaadin.shared.util.SharedUtil; import com.vaadin.v7.data.Property; import com.vaadin.v7.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. * @since 3.0 * * @deprecated As of 8.0, replaced by {@link ValueProvider}, {@link Setter}, see {@link Binder} */ @Deprecated @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; private static final Object[] DEFAULT_GET_ARGS = new Object[0]; private static final Object[] DEFAULT_SET_ARGS = new Object[1]; /* 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(); Object[] setArgs = (Object[]) in.readObject(); Object[] getArgs = (Object[]) in.readObject(); setArguments(getArgs, setArgs, setArgumentIndex); String name = (String) in.readObject(); Class<?>[] paramTypes = SerializerHelper.readClassArray(in); if (instance != null && name != null) { setMethod = instance.getClass().getMethod(name, paramTypes); } else { setMethod = null; } name = (String) in.readObject(); paramTypes = SerializerHelper.readClassArray(in); if (instance != null && 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). beanPropertyName = SharedUtil.capitalize(beanPropertyName); // 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(DEFAULT_GET_ARGS, DEFAULT_SET_ARGS, 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 = SharedUtil.capitalize(propertyName); 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; } /** * 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 { if (instance == null) { return null; } else { 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) { if (getArgs.length == 0) { this.getArgs = DEFAULT_GET_ARGS; } else { this.getArgs = Arrays.copyOf(getArgs, getArgs.length); } if (Arrays.equals(setArgs, DEFAULT_SET_ARGS)) { this.setArgs = DEFAULT_SET_ARGS; } else { this.setArgs = Arrays.copyOf(setArgs, setArgs.length); } 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 public void setValue(T newValue) throws Property.ReadOnlyException { // Checks the mode if (isReadOnly()) { throw new Property.ReadOnlyException(); } invokeSetMethod(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. * @since 3.0 */ @SuppressWarnings("rawtypes") @Deprecated // 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(); } /** * The instance used by this property * * @return the instance used for fetching the property value * @since 7.7.7 */ public Object getInstance() { return instance; } /** * Sets the instance used by this property. * <p> * The new instance must be of the same type as the old instance * <p> * To be consistent with {@link #setValue(Object)}, this method will fire a * value change event even if the value stays the same * * @param instance * the instance to use * @since 7.7.7 */ public void setInstance(Object instance) { if (this.instance.getClass() != instance.getClass()) { throw new IllegalArgumentException("The new instance is of type " + instance.getClass().getName() + " which does not match the old instance type " + this.instance.getClass().getName()); } this.instance = instance; fireValueChange(); } private static final Logger getLogger() { return Logger.getLogger(MethodProperty.class.getName()); } }