/* @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
* 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
* 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.
*
* 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.
*
* 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}
*
setArgs
to be
* replaced with newValue
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
* 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 insetArgs
to be
* replaced with newValue
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 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 extends T> 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 insetArgs
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());
}
}