]> source.dussan.org Git - vaadin-framework.git/commitdiff
#4995 Nested bean property support
authorHenri Sara <henri.sara@itmill.com>
Mon, 18 Apr 2011 12:13:36 +0000 (12:13 +0000)
committerHenri Sara <henri.sara@itmill.com>
Mon, 18 Apr 2011 12:13:36 +0000 (12:13 +0000)
svn changeset:18356/svn branch:6.6

12 files changed:
src/com/vaadin/data/util/AbstractBeanContainer.java
src/com/vaadin/data/util/BeanItem.java
src/com/vaadin/data/util/MethodProperty.java
src/com/vaadin/data/util/MethodPropertyDescriptor.java [new file with mode: 0644]
src/com/vaadin/data/util/NestedMethodProperty.java [new file with mode: 0644]
src/com/vaadin/data/util/NestedPropertyDescriptor.java [new file with mode: 0644]
src/com/vaadin/data/util/VaadinPropertyDescriptor.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/BeanContainerTest.java
tests/src/com/vaadin/tests/server/container/BeanItemContainerTest.java
tests/src/com/vaadin/tests/server/container/BeanItemTest.java
tests/src/com/vaadin/tests/server/container/NestedMethodPropertyTest.java [new file with mode: 0644]
tests/src/com/vaadin/tests/server/container/PropertyDescriptorTest.java [new file with mode: 0644]

index fab61786d12972e3093d4929f96d9b0e4f98de05..1daa4050c54ab0f6495f18501edd7577a2a8c754 100644 (file)
@@ -3,17 +3,16 @@
  */
 package com.vaadin.data.util;
 
-import java.beans.PropertyDescriptor;
-import java.io.IOException;
 import java.io.Serializable;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import com.vaadin.data.Container;
 import com.vaadin.data.Container.Filterable;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
 import com.vaadin.data.Container.SimpleFilterable;
 import com.vaadin.data.Container.Sortable;
 import com.vaadin.data.Item;
@@ -21,6 +20,7 @@ import com.vaadin.data.Property;
 import com.vaadin.data.Property.ValueChangeEvent;
 import com.vaadin.data.Property.ValueChangeListener;
 import com.vaadin.data.Property.ValueChangeNotifier;
+import com.vaadin.data.util.MethodProperty.MethodException;
 import com.vaadin.data.util.filter.SimpleStringFilter;
 import com.vaadin.data.util.filter.UnsupportedFilterException;
 
@@ -40,11 +40,6 @@ import com.vaadin.data.util.filter.UnsupportedFilterException;
  * {@link #addItemAt(int, Object, Object)}.
  * </p>
  * 
- * <p>
- * It is not possible to add additional properties to the container and nested
- * bean properties are not supported.
- * </p>
- * 
  * @param <IDTYPE>
  *            The type of the item identifier
  * @param <BEANTYPE>
@@ -54,7 +49,8 @@ import com.vaadin.data.util.filter.UnsupportedFilterException;
  */
 public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
         AbstractInMemoryContainer<IDTYPE, String, BeanItem<BEANTYPE>> implements
-        Filterable, SimpleFilterable, Sortable, ValueChangeListener {
+        Filterable, SimpleFilterable, Sortable, ValueChangeListener,
+        PropertySetChangeNotifier {
 
     /**
      * Resolver that maps beans to their (item) identifiers, removing the need
@@ -90,7 +86,6 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
             BeanIdResolver<IDTYPE, BEANTYPE> {
 
         private final Object propertyId;
-        private transient Method getMethod;
 
         public PropertyBasedBeanIdResolver(Object propertyId) {
             if (propertyId == null) {
@@ -100,25 +95,18 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
             this.propertyId = propertyId;
         }
 
-        private Method getGetter() throws IllegalStateException {
-            if (getMethod == null) {
-                if (!model.containsKey(propertyId)) {
-                    throw new IllegalStateException("Property " + propertyId
-                            + " not found");
-                }
-                getMethod = model.get(propertyId).getReadMethod();
-            }
-            return getMethod;
-        }
-
         @SuppressWarnings("unchecked")
         public IDTYPE getIdForBean(BEANTYPE bean)
                 throws IllegalArgumentException {
+            VaadinPropertyDescriptor<BEANTYPE> pd = model.get(propertyId);
+            if (null == pd) {
+                throw new IllegalStateException("Property " + propertyId
+                        + " not found");
+            }
             try {
-                return (IDTYPE) getGetter().invoke(bean);
-            } catch (IllegalAccessException e) {
-                throw new IllegalArgumentException(e);
-            } catch (InvocationTargetException e) {
+                Property property = pd.createProperty(bean);
+                return (IDTYPE) property.getValue();
+            } catch (MethodException e) {
                 throw new IllegalArgumentException(e);
             }
         }
@@ -149,7 +137,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
      * A description of the properties found in beans of type {@link #type}.
      * Determines the property ids that are present in the container.
      */
-    private transient LinkedHashMap<String, PropertyDescriptor> model;
+    private LinkedHashMap<String, VaadinPropertyDescriptor<BEANTYPE>> model;
 
     /**
      * Constructs a {@code AbstractBeanContainer} for beans of the given type.
@@ -165,17 +153,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
                     "The bean type passed to AbstractBeanContainer must not be null");
         }
         this.type = type;
-        model = BeanItem.getPropertyDescriptors(type);
-    }
-
-    /**
-     * A special deserialization method that resolves {@link #model} is needed
-     * as PropertyDescriptor is not {@link Serializable}.
-     */
-    private void readObject(java.io.ObjectInputStream in) throws IOException,
-            ClassNotFoundException {
-        in.defaultReadObject();
-        model = BeanItem.getPropertyDescriptors(type);
+        model = BeanItem.getPropertyDescriptors((Class<BEANTYPE>) type);
     }
 
     /*
@@ -725,4 +703,95 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
         return new PropertyBasedBeanIdResolver(propertyId);
     }
 
+    @Override
+    public void addListener(Container.PropertySetChangeListener listener) {
+        super.addListener(listener);
+    }
+
+    @Override
+    public void removeListener(Container.PropertySetChangeListener listener) {
+        super.removeListener(listener);
+    }
+
+    @Override
+    public boolean addContainerProperty(Object propertyId, Class<?> type,
+            Object defaultValue) throws UnsupportedOperationException {
+        throw new UnsupportedOperationException(
+                "Use addNestedContainerProperty(String) to add container properties to a "
+                        + getClass().getSimpleName());
+    }
+
+    /**
+     * Adds a property for the container and all its items.
+     * 
+     * Primarily for internal use, may change in future versions.
+     * 
+     * @param propertyId
+     * @param propertyDescriptor
+     * @return true if the property was added
+     */
+    protected final boolean addContainerProperty(String propertyId,
+            VaadinPropertyDescriptor<BEANTYPE> propertyDescriptor) {
+        if (null == propertyId || null == propertyDescriptor) {
+            return false;
+        }
+
+        // Fails if the Property is already present
+        if (model.containsKey(propertyId)) {
+            return false;
+        }
+
+        model.put(propertyId, propertyDescriptor);
+        for (BeanItem item : itemIdToItem.values()) {
+            item.addItemProperty(propertyId, propertyDescriptor
+                    .createProperty((BEANTYPE) item.getBean()));
+        }
+
+        // Sends a change event
+        fireContainerPropertySetChange();
+
+        return true;
+    }
+
+    /**
+     * Adds a nested container property for the container, e.g.
+     * "manager.address.street".
+     * 
+     * All intermediate getters must exist and must return non-null values when
+     * the property value is accessed.
+     * 
+     * @see NestedMethodProperty
+     * 
+     * @param propertyId
+     * @param propertyType
+     * @return true if the property was added
+     */
+    public boolean addNestedContainerProperty(String propertyId,
+            Class<?> propertyType) {
+        return addContainerProperty(propertyId, new NestedPropertyDescriptor(
+                propertyId, propertyType));
+    }
+
+    @Override
+    public boolean removeContainerProperty(Object propertyId)
+            throws UnsupportedOperationException {
+        // Fails if the Property is not present
+        if (!model.containsKey(propertyId)) {
+            return false;
+        }
+
+        // Removes the Property to Property list and types
+        model.remove(propertyId);
+
+        // If remove the Property from all Items
+        for (final Iterator<IDTYPE> i = getAllItemIds().iterator(); i.hasNext();) {
+            getUnfilteredItem(i.next()).removeItemProperty(propertyId);
+        }
+
+        // Sends a change event
+        fireContainerPropertySetChange();
+
+        return true;
+    }
+
 }
index eb0d2f44c3525b9ea650d7da64043f9bc441ff8e..bb8dd6cdc21ee3670dd9c9d8262dc0561a0d77d6 100644 (file)
@@ -14,8 +14,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
-
-import com.vaadin.data.Property;
+import java.util.Map;
 
 /**
  * A wrapper class for adding the Item interface to any Java Bean.
@@ -51,7 +50,7 @@ public class BeanItem<BT> extends PropertysetItem {
      * 
      */
     public BeanItem(BT bean) {
-        this(bean, getPropertyDescriptors(bean.getClass()));
+        this(bean, getPropertyDescriptors((Class<BT>) bean.getClass()));
     }
 
     /**
@@ -67,19 +66,12 @@ public class BeanItem<BT> extends PropertysetItem {
      *            pre-computed property descriptors
      */
     BeanItem(BT bean,
-            LinkedHashMap<String, PropertyDescriptor> propertyDescriptors) {
+            Map<String, VaadinPropertyDescriptor<BT>> propertyDescriptors) {
 
         this.bean = bean;
 
-        for (PropertyDescriptor pd : propertyDescriptors.values()) {
-            final Method getMethod = pd.getReadMethod();
-            final Method setMethod = pd.getWriteMethod();
-            final Class<?> type = pd.getPropertyType();
-            final String name = pd.getName();
-            final Property p = new MethodProperty<Object>(type, bean,
-                    getMethod, setMethod);
-            addItemProperty(name, p);
-
+        for (VaadinPropertyDescriptor<BT> pd : propertyDescriptors.values()) {
+            addItemProperty(pd.getName(), pd.createProperty(bean));
         }
     }
 
@@ -106,20 +98,14 @@ public class BeanItem<BT> extends PropertysetItem {
         this.bean = bean;
 
         // Create bean information
-        LinkedHashMap<String, PropertyDescriptor> pds = getPropertyDescriptors(bean
+        LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pds = getPropertyDescriptors((Class<BT>) bean
                 .getClass());
 
         // Add all the bean properties as MethodProperties to this Item
         for (Object id : propertyIds) {
-            PropertyDescriptor pd = pds.get(id);
+            VaadinPropertyDescriptor<BT> pd = pds.get(id);
             if (pd != null) {
-                final String name = pd.getName();
-                final Method getMethod = pd.getReadMethod();
-                final Method setMethod = pd.getWriteMethod();
-                final Class<?> type = pd.getPropertyType();
-                final Property p = new MethodProperty<Object>(type, bean,
-                        getMethod, setMethod);
-                addItemProperty(name, p);
+                addItemProperty(pd.getName(), pd.createProperty(bean));
             }
         }
 
@@ -162,9 +148,9 @@ public class BeanItem<BT> extends PropertysetItem {
      *            the Java Bean class to get properties for.
      * @return an ordered map from property names to property descriptors
      */
-    static LinkedHashMap<String, PropertyDescriptor> getPropertyDescriptors(
-            final Class<?> beanClass) {
-        final LinkedHashMap<String, PropertyDescriptor> pdMap = new LinkedHashMap<String, PropertyDescriptor>();
+    static <BT> LinkedHashMap<String, VaadinPropertyDescriptor<BT>> getPropertyDescriptors(
+            final Class<BT> beanClass) {
+        final LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pdMap = new LinkedHashMap<String, VaadinPropertyDescriptor<BT>>();
 
         // Try to introspect, if it fails, we just have an empty Item
         try {
@@ -176,7 +162,10 @@ public class BeanItem<BT> extends PropertysetItem {
                 final Method getMethod = pd.getReadMethod();
                 if ((getMethod != null)
                         && getMethod.getDeclaringClass() != Object.class) {
-                    pdMap.put(pd.getName(), pd);
+                    VaadinPropertyDescriptor<BT> vaadinPropertyDescriptor = new MethodPropertyDescriptor(
+                            pd.getName(), pd.getPropertyType(),
+                            pd.getReadMethod(), pd.getWriteMethod());
+                    pdMap.put(pd.getName(), vaadinPropertyDescriptor);
                 }
             }
         } catch (final java.beans.IntrospectionException ignored) {
index 6cfa5e87b7418bae3c4d51d1efe37ed5c10b4046..12b69858dac3138c349665c42665affda3000aab 100644 (file)
@@ -106,19 +106,23 @@ public class MethodProperty<T> implements Property,
         out.writeObject(getArgs);
         if (setMethod != null) {
             out.writeObject(setMethod.getName());
+            SerializerHelper.writeClass(out, setMethod.getDeclaringClass());
             SerializerHelper
                     .writeClassArray(out, setMethod.getParameterTypes());
         } else {
             out.writeObject(null);
             out.writeObject(null);
+            out.writeObject(null);
         }
         if (getMethod != null) {
             out.writeObject(getMethod.getName());
+            SerializerHelper.writeClass(out, getMethod.getDeclaringClass());
             SerializerHelper
                     .writeClassArray(out, getMethod.getParameterTypes());
         } else {
             out.writeObject(null);
             out.writeObject(null);
+            out.writeObject(null);
         }
     };
 
@@ -135,17 +139,19 @@ public class MethodProperty<T> implements Property,
             setArgs = (Object[]) in.readObject();
             getArgs = (Object[]) in.readObject();
             String name = (String) in.readObject();
+            Class<T> setMethodClass = (Class<T>) SerializerHelper.readClass(in);
             Class<?>[] paramTypes = SerializerHelper.readClassArray(in);
             if (name != null) {
-                setMethod = instance.getClass().getMethod(name, paramTypes);
+                setMethod = setMethodClass.getMethod(name, paramTypes);
             } else {
                 setMethod = null;
             }
 
             name = (String) in.readObject();
+            Class<T> getMethodClass = (Class<T>) SerializerHelper.readClass(in);
             paramTypes = SerializerHelper.readClassArray(in);
             if (name != null) {
-                getMethod = instance.getClass().getMethod(name, paramTypes);
+                getMethod = getMethodClass.getMethod(name, paramTypes);
             } else {
                 getMethod = null;
             }
@@ -202,21 +208,10 @@ public class MethodProperty<T> implements Property,
         // Find the get method
         getMethod = null;
         try {
-            getMethod = beanClass.getMethod("get" + beanPropertyName,
-                    new Class[] {});
+            getMethod = initGetterMethod(beanPropertyName, beanClass);
         } catch (final java.lang.NoSuchMethodException ignored) {
-            try {
-                getMethod = beanClass.getMethod("is" + beanPropertyName,
-                        new Class[] {});
-            } catch (final java.lang.NoSuchMethodException ignoredAsWell) {
-                try {
-                    getMethod = beanClass.getMethod("are" + beanPropertyName,
-                            new Class[] {});
-                } catch (final java.lang.NoSuchMethodException e) {
-                    throw new MethodException(this, "Bean property "
-                            + beanPropertyName + " can not be found");
-                }
-            }
+            throw new MethodException(this, "Bean property " + beanPropertyName
+                    + " can not be found");
         }
 
         // In case the get method is found, resolve the type
@@ -232,23 +227,8 @@ public class MethodProperty<T> implements Property,
 
         // Gets the return type from get method
         if (returnType.isPrimitive()) {
-            if (returnType.equals(Boolean.TYPE)) {
-                type = (Class<T>) Boolean.class;
-            } else if (returnType.equals(Integer.TYPE)) {
-                type = (Class<T>) Integer.class;
-            } else if (returnType.equals(Float.TYPE)) {
-                type = (Class<T>) Float.class;
-            } else if (returnType.equals(Double.TYPE)) {
-                type = (Class<T>) Double.class;
-            } else if (returnType.equals(Byte.TYPE)) {
-                type = (Class<T>) Byte.class;
-            } else if (returnType.equals(Character.TYPE)) {
-                type = (Class<T>) Character.class;
-            } else if (returnType.equals(Short.TYPE)) {
-                type = (Class<T>) Short.class;
-            } else if (returnType.equals(Long.TYPE)) {
-                type = (Class<T>) Long.class;
-            } else {
+            type = (Class<T>) convertPrimitiveType(returnType);
+            if (type.isPrimitive()) {
                 throw new MethodException(this, "Bean property "
                         + beanPropertyName
                         + " getter return type must not be void");
@@ -484,25 +464,7 @@ public class MethodProperty<T> implements Property,
         }
 
         // Gets the return type from get method
-        if (type.isPrimitive()) {
-            if (type.equals(Boolean.TYPE)) {
-                this.type = (Class<T>) Boolean.class;
-            } else if (type.equals(Integer.TYPE)) {
-                this.type = (Class<T>) Integer.class;
-            } else if (type.equals(Float.TYPE)) {
-                this.type = (Class<T>) Float.class;
-            } else if (type.equals(Double.TYPE)) {
-                this.type = (Class<T>) Double.class;
-            } else if (type.equals(Byte.TYPE)) {
-                this.type = (Class<T>) Byte.class;
-            } else if (type.equals(Character.TYPE)) {
-                this.type = (Class<T>) Character.class;
-            } else if (type.equals(Short.TYPE)) {
-                this.type = (Class<T>) Short.class;
-            } else if (type.equals(Long.TYPE)) {
-                this.type = (Class<T>) Long.class;
-            }
-        }
+        this.type = (Class<T>) convertPrimitiveType(type);
 
         setArguments(getArgs, setArgs, setArgumentIndex);
         readOnly = (setMethod == null);
@@ -559,6 +521,50 @@ public class MethodProperty<T> implements Property,
             }
         }
 
+        // Gets the return type from get method
+        type = convertPrimitiveType(type);
+
+        this.getMethod = getMethod;
+        this.setMethod = setMethod;
+        setArguments(getArgs, setArgs, setArgumentIndex);
+        readOnly = (setMethod == null);
+        this.instance = instance;
+        this.type = type;
+    }
+
+    /**
+     * 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
+     */
+    protected 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;
+    }
+
+    protected static Class<?> convertPrimitiveType(Class<?> type) {
         // Gets the return type from get method
         if (type.isPrimitive()) {
             if (type.equals(Boolean.TYPE)) {
@@ -579,13 +585,7 @@ public class MethodProperty<T> implements Property,
                 type = Long.class;
             }
         }
-
-        this.getMethod = getMethod;
-        this.setMethod = setMethod;
-        setArguments(getArgs, setArgs, setArgumentIndex);
-        readOnly = (setMethod == null);
-        this.instance = instance;
-        this.type = type;
+        return type;
     }
 
     /**
@@ -725,7 +725,7 @@ public class MethodProperty<T> implements Property,
      * 
      * @param value
      */
-    private void invokeSetMethod(Object value) {
+    protected void invokeSetMethod(Object value) {
 
         try {
             // Construct a temporary argument array only if needed
@@ -748,6 +748,25 @@ public class MethodProperty<T> implements Property,
         }
     }
 
+    /**
+     * Returns the bean instance which to which the property applies. For
+     * internal use.
+     * 
+     * @return bean instance
+     */
+    protected Object getInstance() {
+        return instance;
+    }
+
+    /**
+     * Returns the setter method to use. For internal use.
+     * 
+     * @return setter {@link Method}
+     */
+    protected Method getSetMethod() {
+        return setMethod;
+    }
+
     /**
      * Sets the Property's read-only mode to the specified status.
      * 
diff --git a/src/com/vaadin/data/util/MethodPropertyDescriptor.java b/src/com/vaadin/data/util/MethodPropertyDescriptor.java
new file mode 100644 (file)
index 0000000..4f366d1
--- /dev/null
@@ -0,0 +1,128 @@
+package com.vaadin.data.util;
+
+import java.io.IOException;
+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;
+
+/**
+ * Property descriptor that is able to create simple {@link MethodProperty}
+ * instances for a bean, using given accessors.
+ * 
+ * @param <BT>
+ *            bean type
+ * 
+ * @since 6.6
+ */
+public class MethodPropertyDescriptor<BT> implements
+        VaadinPropertyDescriptor<BT> {
+
+    private static final Logger logger = Logger
+            .getLogger(MethodPropertyDescriptor.class.getName());
+
+    private final String name;
+    private Class<?> propertyType;
+    private transient Method readMethod;
+    private transient Method writeMethod;
+
+    /**
+     * Creates a property descriptor that can create MethodProperty instances to
+     * access the underlying bean property.
+     * 
+     * @param name
+     *            of the property
+     * @param propertyType
+     *            type (class) of the property
+     * @param readMethod
+     *            getter {@link Method} for the property
+     * @param writeMethod
+     *            setter {@link Method} for the property or null if read-only
+     *            property
+     */
+    public MethodPropertyDescriptor(String name, Class<?> propertyType,
+            Method readMethod, Method writeMethod) {
+        this.name = name;
+        this.propertyType = propertyType;
+        this.readMethod = readMethod;
+        this.writeMethod = writeMethod;
+    }
+
+    /* Special serialization to handle method references */
+    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+        out.defaultWriteObject();
+        SerializerHelper.writeClass(out, propertyType);
+
+        if (writeMethod != null) {
+            out.writeObject(writeMethod.getName());
+            SerializerHelper.writeClass(out, writeMethod.getDeclaringClass());
+            SerializerHelper.writeClassArray(out,
+                    writeMethod.getParameterTypes());
+        } else {
+            out.writeObject(null);
+            out.writeObject(null);
+            out.writeObject(null);
+        }
+
+        if (readMethod != null) {
+            out.writeObject(readMethod.getName());
+            SerializerHelper.writeClass(out, readMethod.getDeclaringClass());
+            SerializerHelper.writeClassArray(out,
+                    readMethod.getParameterTypes());
+        } else {
+            out.writeObject(null);
+            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<BT> class1 = (Class<BT>) SerializerHelper.readClass(in);
+            propertyType = class1;
+
+            String name = (String) in.readObject();
+            Class<?> writeMethodClass = SerializerHelper.readClass(in);
+            Class<?>[] paramTypes = SerializerHelper.readClassArray(in);
+            if (name != null) {
+                writeMethod = writeMethodClass.getMethod(name, paramTypes);
+            } else {
+                writeMethod = null;
+            }
+
+            name = (String) in.readObject();
+            Class<?> readMethodClass = SerializerHelper.readClass(in);
+            paramTypes = SerializerHelper.readClassArray(in);
+            if (name != null) {
+                readMethod = readMethodClass.getMethod(name, paramTypes);
+            } else {
+                readMethod = null;
+            }
+        } catch (SecurityException e) {
+            logger.log(Level.SEVERE, "Internal deserialization error", e);
+        } catch (NoSuchMethodException e) {
+            logger.log(Level.SEVERE, "Internal deserialization error", e);
+        }
+    };
+
+    public String getName() {
+        return name;
+    }
+
+    public Class<?> getPropertyType() {
+        return propertyType;
+    }
+
+    public Property createProperty(Object bean) {
+        return new MethodProperty<Object>(propertyType, bean, readMethod,
+                writeMethod);
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/vaadin/data/util/NestedMethodProperty.java b/src/com/vaadin/data/util/NestedMethodProperty.java
new file mode 100644 (file)
index 0000000..1225021
--- /dev/null
@@ -0,0 +1,200 @@
+package com.vaadin.data.util;
+
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 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
+ * 
+ * @param <T>
+ *            property type
+ * 
+ * @since 6.6
+ */
+public class NestedMethodProperty<T> extends MethodProperty<T> {
+
+    // needed for de-serialization
+    private String propertyName;
+
+    // chain of getter methods up to but not including the last method handled
+    // by the superclass
+    private transient List<Method> getMethods;
+
+    /* Special serialization to handle method references */
+    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+        out.defaultWriteObject();
+        // getMethods is reconstructed on read based on propertyName
+    }
+
+    /* Special serialization to handle method references */
+    private void readObject(java.io.ObjectInputStream in) throws IOException,
+            ClassNotFoundException {
+        in.defaultReadObject();
+
+        // re-build getMethods: some duplicated code with builder method
+        getMethods = new ArrayList<Method>();
+        Class<?> propertyClass = getInstance().getClass();
+        String[] simplePropertyNames = propertyName.split("\\.");
+        for (int i = 0; i < simplePropertyNames.length; i++) {
+            String simplePropertyName = simplePropertyNames[i].trim();
+            try {
+                Method getter = initGetterMethod(simplePropertyName,
+                        propertyClass);
+                propertyClass = getter.getReturnType();
+                getMethods.add(getter);
+            } catch (final java.lang.NoSuchMethodException e) {
+                throw new InvalidObjectException("Bean property '"
+                        + simplePropertyName + "' not found");
+            }
+        }
+    }
+
+    /**
+     * 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 <T>
+     *            property type (deepest nested property)
+     * @param instance
+     *            top-level bean to which the property applies
+     * @param propertyName
+     *            dot separated nested property name
+     * @return new NestedMethodProperty instance
+     */
+    public static <T> NestedMethodProperty<T> buildNestedMethodProperty(
+            Object instance, String propertyName) {
+        List<Method> getMethods = new ArrayList<Method>();
+
+        String lastSimplePropertyName = propertyName;
+        Class<?> lastClass = instance.getClass();
+
+        // first top-level property, then go deeper in a loop
+        Class<?> propertyClass = instance.getClass();
+        String[] simplePropertyNames = propertyName.split("\\.");
+        if (propertyName.endsWith(".") || 0 == simplePropertyNames.length) {
+            throw new MethodException(null, "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 = initGetterMethod(simplePropertyName,
+                            propertyClass);
+                    propertyClass = getter.getReturnType();
+                    getMethods.add(getter);
+                } catch (final java.lang.NoSuchMethodException e) {
+                    throw new MethodException(null, "Bean property '"
+                            + simplePropertyName + "' not found");
+                }
+            } else {
+                throw new MethodException(null,
+                        "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) {
+        }
+
+        NestedMethodProperty<T> property = new NestedMethodProperty<T>(
+                (Class<T>) convertPrimitiveType(type), instance, propertyName,
+                lastGetMethod, setMethod);
+        property.getMethods = getMethods;
+
+        return property;
+    }
+
+    protected NestedMethodProperty(Class<T> type, Object instance,
+            String propertyName, Method lastGetMethod, Method setMethod) {
+        super(type, instance, lastGetMethod, setMethod);
+        this.propertyName = propertyName;
+    }
+
+    /**
+     * 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 Object getValue() {
+        try {
+            Object instance = getInstance();
+            for (Method m : getMethods) {
+                instance = m.invoke(instance);
+            }
+            return instance;
+        } catch (final Throwable e) {
+            throw new MethodException(this, e);
+        }
+    }
+
+    /**
+     * Internal method to actually call the setter method of the wrapped
+     * property.
+     * 
+     * @param value
+     */
+    @Override
+    protected void invokeSetMethod(Object value) {
+        try {
+            Object instance = getInstance();
+            for (int i = 0; i < getMethods.size() - 1; i++) {
+                instance = getMethods.get(i).invoke(instance);
+            }
+            getSetMethod().invoke(instance, 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<Method> getGetMethods() {
+        return Collections.unmodifiableList(getMethods);
+    }
+
+}
diff --git a/src/com/vaadin/data/util/NestedPropertyDescriptor.java b/src/com/vaadin/data/util/NestedPropertyDescriptor.java
new file mode 100644 (file)
index 0000000..e916592
--- /dev/null
@@ -0,0 +1,51 @@
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+
+/**
+ * Property descriptor that is able to create nested property instances for a
+ * bean.
+ * 
+ * The property is specified in the dotted notation, e.g. "address.street", and
+ * can contain multiple levels of nesting.
+ * 
+ * @param <BT>
+ *            bean type
+ * 
+ * @since 6.6
+ */
+public class NestedPropertyDescriptor<BT> implements
+        VaadinPropertyDescriptor<BT> {
+
+    private final String name;
+    private final Class<?> propertyType;
+
+    /**
+     * Creates a property descriptor that can create MethodProperty instances to
+     * access the underlying bean property.
+     * 
+     * The property is only validated when trying to access its value.
+     * 
+     * @param name
+     *            of the property in a dotted path format, e.g. "address.street"
+     * @param propertyType
+     *            type (class) of the property
+     */
+    public NestedPropertyDescriptor(String name, Class<?> propertyType) {
+        this.name = name;
+        this.propertyType = propertyType;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Class<?> getPropertyType() {
+        return propertyType;
+    }
+
+    public Property createProperty(BT bean) {
+        return NestedMethodProperty.buildNestedMethodProperty(bean, name);
+    }
+
+}
diff --git a/src/com/vaadin/data/util/VaadinPropertyDescriptor.java b/src/com/vaadin/data/util/VaadinPropertyDescriptor.java
new file mode 100644 (file)
index 0000000..b6ae89f
--- /dev/null
@@ -0,0 +1,40 @@
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Property;
+
+/**
+ * Property descriptor that can create a property instance for a bean.
+ * 
+ * Used by {@link BeanItem} and {@link AbstractBeanContainer} to keep track of
+ * the set of properties of items.
+ * 
+ * @param <BT>
+ *            bean type
+ * 
+ * @since 6.6
+ */
+public interface VaadinPropertyDescriptor<BT> extends Serializable {
+    /**
+     * Returns the name of the property.
+     * 
+     * @return
+     */
+    public String getName();
+
+    /**
+     * Returns the type of the property.
+     * 
+     * @return Class<?>
+     */
+    public Class<?> getPropertyType();
+
+    /**
+     * Creates a new {@link Property} instance for this property for a bean.
+     * 
+     * @param bean
+     * @return
+     */
+    public Property createProperty(BT bean);
+}
\ No newline at end of file
index 1641dd996ebfb90bf010cf0cee31ca63b874958c..921132f858e895f799376d9a4526a4f85c2566c5 100644 (file)
@@ -236,16 +236,25 @@ public class BeanContainerTest extends AbstractBeanContainerTest {
             // should get exception
         }
 
-        try {
-            container.removeContainerProperty("name");
-            Assert.fail();
-        } catch (UnsupportedOperationException e) {
-            // should get exception
-        }
-
         assertEquals(1, container.size());
     }
 
+    public void testRemoveContainerProperty() {
+        BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+                Person.class);
+        container.setBeanIdResolver(new PersonNameResolver());
+        container.addBean(new Person("John"));
+
+        Assert.assertEquals("John",
+                container.getContainerProperty("John", "name").getValue());
+        Assert.assertTrue(container.removeContainerProperty("name"));
+        Assert.assertNull(container.getContainerProperty("John", "name"));
+
+        Assert.assertNotNull(container.getItem("John"));
+        // property removed also from item
+        Assert.assertNull(container.getItem("John").getItemProperty("name"));
+    }
+
     public void testAddNullBeans() {
         BeanContainer<String, Person> container = new BeanContainer<String, Person>(
                 Person.class);
@@ -399,4 +408,19 @@ public class BeanContainerTest extends AbstractBeanContainerTest {
         assertEquals(5, container.size());
     }
 
+    public void testAddNestedContainerProperty() {
+        BeanContainer<String, NestedMethodPropertyTest.Person> container = new BeanContainer<String, NestedMethodPropertyTest.Person>(
+                NestedMethodPropertyTest.Person.class);
+        container.setBeanIdProperty("name");
+
+        container.addBean(new NestedMethodPropertyTest.Person("John",
+                new NestedMethodPropertyTest.Address("Ruukinkatu 2-4", 20540)));
+
+        assertTrue(container.addNestedContainerProperty("address.street",
+                String.class));
+        assertEquals("Ruukinkatu 2-4",
+                container.getContainerProperty("John", "address.street")
+                        .getValue());
+    }
+
 }
index c9d9d218029396f52b87dc43d1bba1e1a87b277d..60ea01fdc7e71ee776a42167f250d08466487d38 100644 (file)
@@ -651,16 +651,25 @@ public class BeanItemContainerTest extends AbstractBeanContainerTest {
             // should get exception
         }
 
-        try {
-            container.removeContainerProperty("name");
-            Assert.fail();
-        } catch (UnsupportedOperationException e) {
-            // should get exception
-        }
-
         assertEquals(1, container.size());
     }
 
+    public void testRemoveContainerProperty() {
+        BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+                Person.class);
+        Person john = new Person("John");
+        container.addBean(john);
+
+        Assert.assertEquals("John", container
+                .getContainerProperty(john, "name").getValue());
+        Assert.assertTrue(container.removeContainerProperty("name"));
+        Assert.assertNull(container.getContainerProperty(john, "name"));
+
+        Assert.assertNotNull(container.getItem(john));
+        // property removed also from item
+        Assert.assertNull(container.getItem(john).getItemProperty("name"));
+    }
+
     public void testAddNullBean() {
         BeanItemContainer<Person> container = new BeanItemContainer<Person>(
                 Person.class);
@@ -691,4 +700,20 @@ public class BeanItemContainerTest extends AbstractBeanContainerTest {
         }
     }
 
+    public void testAddNestedContainerProperty() {
+        BeanItemContainer<NestedMethodPropertyTest.Person> container = new BeanItemContainer<NestedMethodPropertyTest.Person>(
+                NestedMethodPropertyTest.Person.class);
+
+        NestedMethodPropertyTest.Person john = new NestedMethodPropertyTest.Person(
+                "John", new NestedMethodPropertyTest.Address("Ruukinkatu 2-4",
+                        20540));
+        container.addBean(john);
+
+        assertTrue(container.addNestedContainerProperty("address.street",
+                String.class));
+        assertEquals("Ruukinkatu 2-4",
+                container.getContainerProperty(john, "address.street")
+                        .getValue());
+    }
+
 }
index dcd7c756d8e304b39ba283d5770d84cbd2364553..70c419fbbdbfe36246d46fa1cc49aad4d54f6eff 100644 (file)
@@ -1,17 +1,21 @@
 package com.vaadin.tests.server.container;
 
-import java.beans.PropertyDescriptor;
+import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.Map;
 
 import junit.framework.Assert;
 import junit.framework.TestCase;
 
 import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.util.MethodProperty;
+import com.vaadin.data.util.MethodPropertyDescriptor;
+import com.vaadin.data.util.VaadinPropertyDescriptor;
 
 /**
  * Test BeanItem specific features.
@@ -175,14 +179,16 @@ public class BeanItemTest extends TestCase {
         Method method = BeanItem.class.getDeclaredMethod(
                 "getPropertyDescriptors", Class.class);
         method.setAccessible(true);
-        LinkedHashMap<String, PropertyDescriptor> propertyDescriptors = (LinkedHashMap<String, PropertyDescriptor>) method
+        LinkedHashMap<String, VaadinPropertyDescriptor<Class>> propertyDescriptors = (LinkedHashMap<String, VaadinPropertyDescriptor<Class>>) method
                 .invoke(null, MySuperInterface.class);
 
         Assert.assertEquals(2, propertyDescriptors.size());
         Assert.assertTrue(propertyDescriptors.containsKey("super1"));
         Assert.assertTrue(propertyDescriptors.containsKey("override"));
 
-        Assert.assertNull(propertyDescriptors.get("override").getWriteMethod());
+        MethodProperty<?> property = (MethodProperty<?>) propertyDescriptors
+                .get("override").createProperty(getClass());
+        Assert.assertTrue(property.isReadOnly());
     }
 
     public void testGetSuperInterfaceProperties() throws SecurityException,
@@ -191,7 +197,7 @@ public class BeanItemTest extends TestCase {
         Method method = BeanItem.class.getDeclaredMethod(
                 "getPropertyDescriptors", Class.class);
         method.setAccessible(true);
-        LinkedHashMap<String, PropertyDescriptor> propertyDescriptors = (LinkedHashMap<String, PropertyDescriptor>) method
+        LinkedHashMap<String, VaadinPropertyDescriptor<Class>> propertyDescriptors = (LinkedHashMap<String, VaadinPropertyDescriptor<Class>>) method
                 .invoke(null, MySubInterface.class);
 
         Assert.assertEquals(4, propertyDescriptors.size());
@@ -200,8 +206,9 @@ public class BeanItemTest extends TestCase {
         Assert.assertTrue(propertyDescriptors.containsKey("super2"));
         Assert.assertTrue(propertyDescriptors.containsKey("override"));
 
-        Assert.assertNotNull(propertyDescriptors.get("override")
-                .getWriteMethod());
+        MethodProperty<?> property = (MethodProperty<?>) propertyDescriptors
+                .get("override").createProperty(getClass());
+        Assert.assertFalse(property.isReadOnly());
     }
 
     public void testPropertyExplicitOrder() {
@@ -290,4 +297,41 @@ public class BeanItemTest extends TestCase {
         Assert.assertTrue(item.getItemProperty("name2").isReadOnly());
     }
 
+    public void testCustomProperties() throws Exception {
+        LinkedHashMap<String, VaadinPropertyDescriptor<MyClass>> propertyDescriptors = new LinkedHashMap<String, VaadinPropertyDescriptor<MyClass>>();
+        propertyDescriptors.put(
+                "myname",
+                new MethodPropertyDescriptor<BeanItemTest.MyClass>("myname",
+                        MyClass.class, MyClass.class
+                                .getDeclaredMethod("getName"), MyClass.class
+                                .getDeclaredMethod("setName", String.class)));
+        MyClass instance = new MyClass("bean1");
+        Constructor<BeanItem> constructor = BeanItem.class
+                .getDeclaredConstructor(Object.class, Map.class);
+        constructor.setAccessible(true);
+        BeanItem<MyClass> item = constructor.newInstance(instance,
+                propertyDescriptors);
+
+        Assert.assertEquals(1, item.getItemPropertyIds().size());
+        Assert.assertEquals("bean1", item.getItemProperty("myname").getValue());
+    }
+
+    public void testAddRemoveProperty() throws Exception {
+        MethodPropertyDescriptor<BeanItemTest.MyClass> pd = new MethodPropertyDescriptor<BeanItemTest.MyClass>(
+                "myname", MyClass.class,
+                MyClass.class.getDeclaredMethod("getName"),
+                MyClass.class.getDeclaredMethod("setName", String.class));
+
+        BeanItem<MyClass> item = new BeanItem(new MyClass("bean1"));
+
+        Assert.assertEquals(6, item.getItemPropertyIds().size());
+        Assert.assertEquals(null, item.getItemProperty("myname"));
+
+        item.addItemProperty("myname", pd.createProperty(item.getBean()));
+        Assert.assertEquals(7, item.getItemPropertyIds().size());
+        Assert.assertEquals("bean1", item.getItemProperty("myname").getValue());
+        item.removeItemProperty("myname");
+        Assert.assertEquals(6, item.getItemPropertyIds().size());
+        Assert.assertEquals(null, item.getItemProperty("myname"));
+    }
 }
diff --git a/tests/src/com/vaadin/tests/server/container/NestedMethodPropertyTest.java b/tests/src/com/vaadin/tests/server/container/NestedMethodPropertyTest.java
new file mode 100644 (file)
index 0000000..e344970
--- /dev/null
@@ -0,0 +1,334 @@
+package com.vaadin.tests.server.container;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import com.vaadin.data.util.MethodProperty.MethodException;
+import com.vaadin.data.util.NestedMethodProperty;
+
+public class NestedMethodPropertyTest extends TestCase {
+
+    public static class Address implements Serializable {
+        private String street;
+        private int postalCodePrimitive;
+        private Integer postalCodeObject;
+
+        public Address(String street, int postalCode) {
+            this.street = street;
+            postalCodePrimitive = postalCode;
+            postalCodeObject = postalCode;
+        }
+
+        public void setStreet(String street) {
+            this.street = street;
+        }
+
+        public String getStreet() {
+            return street;
+        }
+
+        public void setPostalCodePrimitive(int postalCodePrimitive) {
+            this.postalCodePrimitive = postalCodePrimitive;
+        }
+
+        public int getPostalCodePrimitive() {
+            return postalCodePrimitive;
+        }
+
+        public void setPostalCodeObject(Integer postalCodeObject) {
+            this.postalCodeObject = postalCodeObject;
+        }
+
+        public Integer getPostalCodeObject() {
+            return postalCodeObject;
+        }
+
+        // read-only boolean property
+        public boolean isBoolean() {
+            return true;
+        }
+    }
+
+    public static class Person implements Serializable {
+        private String name;
+        private Address address;
+
+        public Person(String name, Address address) {
+            this.name = name;
+            this.address = address;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setAddress(Address address) {
+            this.address = address;
+        }
+
+        public Address getAddress() {
+            return address;
+        }
+    }
+
+    public static class Team implements Serializable {
+        private String name;
+        private Person manager;
+
+        public Team(String name, Person manager) {
+            this.name = name;
+            this.manager = manager;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setManager(Person manager) {
+            this.manager = manager;
+        }
+
+        public Person getManager() {
+            return manager;
+        }
+    }
+
+    private Address oldMill;
+    private Person joonas;
+    private Team vaadin;
+
+    @Override
+    public void setUp() {
+        oldMill = new Address("Ruukinkatu 2-4", 20540);
+        joonas = new Person("Joonas", oldMill);
+        vaadin = new Team("Vaadin", joonas);
+    }
+
+    @Override
+    public void tearDown() {
+        vaadin = null;
+        joonas = null;
+        oldMill = null;
+    }
+
+    public void testSingleLevelNestedSimpleProperty() {
+        NestedMethodProperty<String> nameProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "name");
+
+        Assert.assertEquals(String.class, nameProperty.getType());
+        Assert.assertEquals("Vaadin", nameProperty.getValue());
+    }
+
+    public void testSingleLevelNestedObjectProperty() {
+        NestedMethodProperty<Person> managerProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager");
+
+        Assert.assertEquals(Person.class, managerProperty.getType());
+        Assert.assertEquals(joonas, managerProperty.getValue());
+    }
+
+    public void testMultiLevelNestedProperty() {
+        NestedMethodProperty<String> managerNameProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.name");
+        NestedMethodProperty<Address> addressProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address");
+        NestedMethodProperty<String> streetProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address.street");
+        NestedMethodProperty<String> postalCodePrimitiveProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin,
+                        "manager.address.postalCodePrimitive");
+        NestedMethodProperty<String> postalCodeObjectProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin,
+                        "manager.address.postalCodeObject");
+        NestedMethodProperty<String> booleanProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address.boolean");
+
+        Assert.assertEquals(String.class, managerNameProperty.getType());
+        Assert.assertEquals("Joonas", managerNameProperty.getValue());
+
+        Assert.assertEquals(Address.class, addressProperty.getType());
+        Assert.assertEquals(oldMill, addressProperty.getValue());
+
+        Assert.assertEquals(String.class, streetProperty.getType());
+        Assert.assertEquals("Ruukinkatu 2-4", streetProperty.getValue());
+
+        Assert.assertEquals(Integer.class,
+                postalCodePrimitiveProperty.getType());
+        Assert.assertEquals(20540, postalCodePrimitiveProperty.getValue());
+
+        Assert.assertEquals(Integer.class, postalCodeObjectProperty.getType());
+        Assert.assertEquals(20540, postalCodeObjectProperty.getValue());
+
+        Assert.assertEquals(Boolean.class, booleanProperty.getType());
+        Assert.assertEquals(true, booleanProperty.getValue());
+    }
+
+    public void testEmptyPropertyName() {
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin, "");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin, " ");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+    }
+
+    public void testInvalidPropertyName() {
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin, ".");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin, ".manager");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin, "manager.");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin,
+                    "manager..name");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+    }
+
+    public void testInvalidNestedPropertyName() {
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin, "member");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin,
+                    "manager.pet");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+
+        try {
+            NestedMethodProperty.buildNestedMethodProperty(vaadin,
+                    "manager.address.city");
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+    }
+
+    public void testNullNestedProperty() {
+        NestedMethodProperty<String> managerNameProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.name");
+        NestedMethodProperty<String> streetProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address.street");
+
+        joonas.setAddress(null);
+        try {
+            streetProperty.getValue();
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+
+        vaadin.setManager(null);
+        try {
+            managerNameProperty.getValue();
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+        try {
+            streetProperty.getValue();
+            fail();
+        } catch (MethodException e) {
+            // should get exception
+        }
+
+        vaadin.setManager(joonas);
+        Assert.assertEquals("Joonas", managerNameProperty.getValue());
+    }
+
+    public void testMultiLevelNestedPropertySetValue() {
+        NestedMethodProperty<String> managerNameProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.name");
+        NestedMethodProperty<Address> addressProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address");
+        NestedMethodProperty<String> streetProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address.street");
+        NestedMethodProperty<String> postalCodePrimitiveProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin,
+                        "manager.address.postalCodePrimitive");
+        NestedMethodProperty<String> postalCodeObjectProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin,
+                        "manager.address.postalCodeObject");
+
+        managerNameProperty.setValue("Joonas L");
+        Assert.assertEquals("Joonas L", joonas.getName());
+        streetProperty.setValue("Ruukinkatu");
+        Assert.assertEquals("Ruukinkatu", oldMill.getStreet());
+        postalCodePrimitiveProperty.setValue(0);
+        postalCodeObjectProperty.setValue(1);
+        Assert.assertEquals(0, oldMill.getPostalCodePrimitive());
+        Assert.assertEquals(Integer.valueOf(1), oldMill.getPostalCodeObject());
+
+        postalCodeObjectProperty.setValue(null);
+        Assert.assertNull(oldMill.getPostalCodeObject());
+
+        Address address2 = new Address("Other street", 12345);
+        addressProperty.setValue(address2);
+        Assert.assertEquals("Other street", streetProperty.getValue());
+    }
+
+    public void testSerialization() throws IOException, ClassNotFoundException {
+        NestedMethodProperty<String> streetProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address.street");
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new ObjectOutputStream(baos).writeObject(streetProperty);
+        NestedMethodProperty<String> property2 = (NestedMethodProperty<String>) new ObjectInputStream(
+                new ByteArrayInputStream(baos.toByteArray())).readObject();
+
+        Assert.assertEquals("Ruukinkatu 2-4", property2.getValue());
+    }
+
+    public void testIsReadOnly() {
+        NestedMethodProperty<String> streetProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address.street");
+        NestedMethodProperty<String> booleanProperty = NestedMethodProperty
+                .buildNestedMethodProperty(vaadin, "manager.address.boolean");
+
+        Assert.assertFalse(streetProperty.isReadOnly());
+        Assert.assertTrue(booleanProperty.isReadOnly());
+    }
+
+}
diff --git a/tests/src/com/vaadin/tests/server/container/PropertyDescriptorTest.java b/tests/src/com/vaadin/tests/server/container/PropertyDescriptorTest.java
new file mode 100644 (file)
index 0000000..69efc5f
--- /dev/null
@@ -0,0 +1,56 @@
+package com.vaadin.tests.server.container;
+
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.util.MethodPropertyDescriptor;
+import com.vaadin.data.util.NestedPropertyDescriptor;
+import com.vaadin.data.util.VaadinPropertyDescriptor;
+import com.vaadin.tests.server.container.NestedMethodPropertyTest.Person;
+
+public class PropertyDescriptorTest extends TestCase {
+    public void testMethodPropertyDescriptorSerialization() throws Exception {
+        PropertyDescriptor[] pds = Introspector.getBeanInfo(Person.class)
+                .getPropertyDescriptors();
+
+        MethodPropertyDescriptor<Person> descriptor = null;
+
+        for (PropertyDescriptor pd : pds) {
+            if ("name".equals(pd.getName())) {
+                descriptor = new MethodPropertyDescriptor<Person>(pd.getName(),
+                        String.class, pd.getReadMethod(), pd.getWriteMethod());
+                break;
+            }
+        }
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new ObjectOutputStream(baos).writeObject(descriptor);
+        VaadinPropertyDescriptor<Person> descriptor2 = (VaadinPropertyDescriptor<Person>) new ObjectInputStream(
+                new ByteArrayInputStream(baos.toByteArray())).readObject();
+
+        Property property = descriptor2
+                .createProperty(new Person("John", null));
+        Assert.assertEquals("John", property.getValue());
+    }
+
+    public void testNestedPropertyDescriptorSerialization() throws Exception {
+        NestedPropertyDescriptor<Person> pd = new NestedPropertyDescriptor<Person>(
+                "name", String.class);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new ObjectOutputStream(baos).writeObject(pd);
+        VaadinPropertyDescriptor<Person> pd2 = (VaadinPropertyDescriptor<Person>) new ObjectInputStream(
+                new ByteArrayInputStream(baos.toByteArray())).readObject();
+
+        Property property = pd2.createProperty(new Person("John", null));
+        Assert.assertEquals("John", property.getValue());
+    }
+}