/* * Copyright 2000-2013 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.data.util; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.vaadin.data.Property; import com.vaadin.data.util.MethodProperty.MethodException; /** * Nested accessor based property for a bean. * * The property is specified in the dotted notation, e.g. "address.street", and * can contain multiple levels of nesting. * * When accessing the property value, all intermediate getters must return * non-null values. * * @see MethodProperty * * @since 6.6 */ public class NestedMethodProperty extends AbstractProperty { // needed for de-serialization private String propertyName; // chain of getter methods private transient List getMethods; /** * The setter method. */ private transient Method setMethod; /** * Bean instance used as a starting point for accessing the property value. */ private Object instance; private Class type; /* Special serialization to handle method references */ private void writeObject(java.io.ObjectOutputStream out) throws IOException { out.defaultWriteObject(); // getMethods and setMethod are reconstructed on read based on // propertyName } /* Special serialization to handle method references */ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); initialize(instance.getClass(), propertyName); } /** * Constructs a nested method property for a given object instance. The * property name is a dot separated string pointing to a nested property, * e.g. "manager.address.street". * * @param instance * top-level bean to which the property applies * @param propertyName * dot separated nested property name * @throws IllegalArgumentException * if the property name is invalid */ public NestedMethodProperty(Object instance, String propertyName) { this.instance = instance; initialize(instance.getClass(), propertyName); } /** * For internal use to deduce property type etc. without a bean instance. * Calling {@link #setValue(Object)} or {@link #getValue()} on properties * constructed this way is not supported. * * @param instanceClass * class of the top-level bean * @param propertyName */ NestedMethodProperty(Class instanceClass, String propertyName) { instance = null; initialize(instanceClass, propertyName); } /** * Initializes most of the internal fields based on the top-level bean * instance and property name (dot-separated string). * * @param beanClass * class of the top-level bean to which the property applies * @param propertyName * dot separated nested property name * @throws IllegalArgumentException * if the property name is invalid */ private void initialize(Class beanClass, String propertyName) throws IllegalArgumentException { List getMethods = new ArrayList(); String lastSimplePropertyName = propertyName; Class lastClass = beanClass; // first top-level property, then go deeper in a loop Class propertyClass = beanClass; String[] simplePropertyNames = propertyName.split("\\."); if (propertyName.endsWith(".") || 0 == simplePropertyNames.length) { throw new IllegalArgumentException("Invalid property name '" + propertyName + "'"); } for (int i = 0; i < simplePropertyNames.length; i++) { String simplePropertyName = simplePropertyNames[i].trim(); if (simplePropertyName.length() > 0) { lastSimplePropertyName = simplePropertyName; lastClass = propertyClass; try { Method getter = MethodProperty.initGetterMethod( simplePropertyName, propertyClass); propertyClass = getter.getReturnType(); getMethods.add(getter); } catch (final java.lang.NoSuchMethodException e) { throw new IllegalArgumentException("Bean property '" + simplePropertyName + "' not found", e); } } else { throw new IllegalArgumentException( "Empty or invalid bean property identifier in '" + propertyName + "'"); } } // In case the get method is found, resolve the type Method lastGetMethod = getMethods.get(getMethods.size() - 1); Class type = lastGetMethod.getReturnType(); // Finds the set method Method setMethod = null; try { // Assure that the first letter is upper cased (it is a common // mistake to write firstName, not FirstName). if (Character.isLowerCase(lastSimplePropertyName.charAt(0))) { final char[] buf = lastSimplePropertyName.toCharArray(); buf[0] = Character.toUpperCase(buf[0]); lastSimplePropertyName = new String(buf); } setMethod = lastClass.getMethod("set" + lastSimplePropertyName, new Class[] { type }); } catch (final NoSuchMethodException skipped) { } this.type = (Class) MethodProperty .convertPrimitiveType(type); this.propertyName = propertyName; this.getMethods = getMethods; this.setMethod = setMethod; } @Override public Class getType() { return type; } @Override public boolean isReadOnly() { return super.isReadOnly() || (null == setMethod); } /** * 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 { Object object = instance; for (Method m : getMethods) { object = m.invoke(object); } return (T) object; } catch (final Throwable e) { throw new MethodException(this, e); } } /** * Sets the value of the property. The new value must be assignable to the * type of this property. * * @param newValue * the New value of the property. * @throws Property.ReadOnlyException if the object is in * read-only mode. * @see #invokeSetMethod(Object) */ @Override public void setValue(T newValue) throws 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 { Object object = instance; for (int i = 0; i < getMethods.size() - 1; i++) { object = getMethods.get(i).invoke(object); } setMethod.invoke(object, new Object[] { value }); } catch (final InvocationTargetException e) { throw new MethodException(this, e.getTargetException()); } catch (final Exception e) { throw new MethodException(this, e); } } /** * Returns an unmodifiable list of getter methods to call in sequence to get * the property value. * * This API may change in future versions. * * @return unmodifiable list of getter methods corresponding to each segment * of the property name */ protected List getGetMethods() { return Collections.unmodifiableList(getMethods); } }