From 9f126db566b14afaa8e025056539a38a5d7b9904 Mon Sep 17 00:00:00 2001 From: Matti Hosio Date: Mon, 15 Jul 2013 15:56:07 +0300 Subject: [PATCH] Support for null intermediate beans in NestedMethodProperty (#11435) Allows intermediate beans to return null in the NestedMethodProperty. The feature is not enabled by default and thus should be fully backwards compatible. Change-Id: I438d0f787c5c76f61ab234f3c92dd927a8354a37 --- .../data/util/AbstractBeanContainer.java | 57 +++++++++++++++++- server/src/com/vaadin/data/util/BeanItem.java | 21 +++++++ .../data/util/NestedMethodProperty.java | 59 ++++++++++++++++++- .../data/util/NestedPropertyDescriptor.java | 24 +++++++- .../vaadin/data/util/BeanContainerTest.java | 24 ++++++++ .../data/util/BeanItemContainerTest.java | 23 ++++++++ .../data/util/NestedMethodPropertyTest.java | 31 ++++++++++ .../data/util/PropertyDescriptorTest.java | 16 +++++ 8 files changed, 250 insertions(+), 5 deletions(-) diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java index 35403d6419..cd5c0c809d 100644 --- a/server/src/com/vaadin/data/util/AbstractBeanContainer.java +++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java @@ -845,8 +845,32 @@ public abstract class AbstractBeanContainer extends * @return true if the property was added */ public boolean addNestedContainerProperty(String propertyId) { + return addNestedContainerProperty(propertyId, false); + } + + /** + * 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 or the nullBeansAllowed must + * be set to true. If the nullBeansAllowed flag is set to true, + * calling getValue of the added property will return null if the property + * or any of its intermediate getters returns null. If set to false, null + * values returned by intermediate getters will cause NullPointerException. + * The default value is false to ensure backwards compatibility. + * + * @see NestedMethodProperty + * + * @param propertyId + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + * @return true if the property was added + */ + public boolean addNestedContainerProperty(String propertyId, + boolean nullBeansAllowed) { return addContainerProperty(propertyId, new NestedPropertyDescriptor( - propertyId, type)); + propertyId, type, nullBeansAllowed)); } /** @@ -864,13 +888,42 @@ public abstract class AbstractBeanContainer extends */ @SuppressWarnings("unchecked") public void addNestedContainerBean(String propertyId) { + addNestedContainerBean(propertyId, false); + } + + /** + * Adds a nested container properties for all sub-properties of a named + * property to the container. The named property itself is removed from the + * model as its subproperties are added. + * + * Unless + * nullBeansAllowed is set to true, all intermediate getters must + * exist and must return non-null values when the property values are + * accessed. If the nullBeansAllowed flag is set to true, + * calling getValue of the added subproperties will return null if the + * property or any of their intermediate getters returns null. If set to + * false, null values returned by intermediate getters will cause + * NullPointerException. The default value is false to ensure backwards + * compatibility. + * + * @see NestedMethodProperty + * @see #addNestedContainerProperty(String) + * + * @param propertyId + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + */ + @SuppressWarnings("unchecked") + public void addNestedContainerBean(String propertyId, + boolean nullBeansAllowed) { Class propertyType = getType(propertyId); LinkedHashMap> pds = BeanItem .getPropertyDescriptors((Class) propertyType); for (String subPropertyId : pds.keySet()) { String qualifiedPropertyId = propertyId + "." + subPropertyId; NestedPropertyDescriptor pd = new NestedPropertyDescriptor( - qualifiedPropertyId, (Class) type); + qualifiedPropertyId, (Class) type, + nullBeansAllowed); model.put(qualifiedPropertyId, pd); model.remove(propertyId); for (BeanItem item : itemIdToItem.values()) { diff --git a/server/src/com/vaadin/data/util/BeanItem.java b/server/src/com/vaadin/data/util/BeanItem.java index fc51be8f36..4834fe4f89 100644 --- a/server/src/com/vaadin/data/util/BeanItem.java +++ b/server/src/com/vaadin/data/util/BeanItem.java @@ -267,6 +267,27 @@ public class BeanItem extends PropertysetItem { getBean(), nestedPropertyId)); } + /** + * Adds a nested property to the item. If the nullBeansAllowed + * flag is set to true, calling getValue of the added property will return + * null if the property or any of its intermediate getters returns null. If + * set to false, null values returned by intermediate getters will cause + * NullPointerException. The default value is false to ensure backwards + * compatibility. + * + * @param nestedPropertyId + * property id to add. This property must not exist in the item + * already and must of of form "field1.field2" where field2 is a + * field in the object referenced to by field1 + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + */ + public void addNestedProperty(String nestedPropertyId, + boolean nullBeansAllowed) { + addItemProperty(nestedPropertyId, new NestedMethodProperty( + getBean(), nestedPropertyId, nullBeansAllowed)); + } + /** * Gets the underlying JavaBean object. * diff --git a/server/src/com/vaadin/data/util/NestedMethodProperty.java b/server/src/com/vaadin/data/util/NestedMethodProperty.java index b62ecfbfc3..7a3963c17e 100644 --- a/server/src/com/vaadin/data/util/NestedMethodProperty.java +++ b/server/src/com/vaadin/data/util/NestedMethodProperty.java @@ -32,7 +32,7 @@ import com.vaadin.data.util.MethodProperty.MethodException; * can contain multiple levels of nesting. * * When accessing the property value, all intermediate getters must return - * non-null values. + * non-null values or the nullBeansAllowed must be set to true. * * @see MethodProperty * @@ -55,6 +55,15 @@ public class NestedMethodProperty extends AbstractProperty { */ private Object instance; + /** + * a boolean flag indicating whether intermediate getters may return null + * values. If the flag is set to true, calling getValue will return null if + * the property or any of its intermediate getters returns null. If set to + * false, intermediate getters returning null value will throw Exception. + * The default value is false to ensure backwards compatibility. + */ + private boolean nullBeansAllowed = false; + private Class type; /* Special serialization to handle method references */ @@ -85,7 +94,33 @@ public class NestedMethodProperty extends AbstractProperty { * if the property name is invalid */ public NestedMethodProperty(Object instance, String propertyName) { + this(instance, propertyName, false); + } + + /** + * 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". The nullBeansAllowed controls + * the behavior in cases where the intermediate getters may return null + * values. If the flag is set to true, calling getValue will return null if + * the property or any of its intermediate getters returns null. If set to + * false, null values returned by intermediate getters will cause + * NullPointerException. The default value is false to ensure backwards + * compatibility. + * + * @param instance + * top-level bean to which the property applies + * @param propertyName + * dot separated nested property name + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + * @throws IllegalArgumentException + * if the property name is invalid + */ + public NestedMethodProperty(Object instance, String propertyName, + boolean nullBeansAllowed) { this.instance = instance; + this.nullBeansAllowed = nullBeansAllowed; initialize(instance.getClass(), propertyName); } @@ -103,6 +138,25 @@ public class NestedMethodProperty extends AbstractProperty { initialize(instanceClass, 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 + * dot separated nested property name + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + */ + NestedMethodProperty(Class instanceClass, String propertyName, + boolean nullBeansAllowed) { + instance = null; + this.nullBeansAllowed = nullBeansAllowed; + initialize(instanceClass, propertyName); + } + /** * Initializes most of the internal fields based on the top-level bean * instance and property name (dot-separated string). @@ -199,6 +253,9 @@ public class NestedMethodProperty extends AbstractProperty { Object object = instance; for (Method m : getMethods) { object = m.invoke(object); + if (object == null && nullBeansAllowed) { + return null; + } } return (T) object; } catch (final Throwable e) { diff --git a/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java index b2055fe776..67eb30fae5 100644 --- a/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java +++ b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java @@ -34,6 +34,7 @@ public class NestedPropertyDescriptor implements private final String name; private final Class propertyType; + private final boolean nullBeansAllowed; /** * Creates a property descriptor that can create MethodProperty instances to @@ -48,10 +49,29 @@ public class NestedPropertyDescriptor implements */ public NestedPropertyDescriptor(String name, Class beanType) throws IllegalArgumentException { + this(name, beanType, false); + } + + /** + * Creates a property descriptor that can create MethodProperty instances to + * access the underlying bean property. + * + * @param name + * of the property in a dotted path format, e.g. "address.street" + * @param beanType + * type (class) of the top-level bean + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + * @throws IllegalArgumentException + * if the property name is invalid + */ + public NestedPropertyDescriptor(String name, Class beanType, + boolean nullBeansAllowed) throws IllegalArgumentException { this.name = name; NestedMethodProperty property = new NestedMethodProperty( - beanType, name); + beanType, name, nullBeansAllowed); this.propertyType = property.getType(); + this.nullBeansAllowed = nullBeansAllowed; } @Override @@ -66,7 +86,7 @@ public class NestedPropertyDescriptor implements @Override public Property createProperty(BT bean) { - return new NestedMethodProperty(bean, name); + return new NestedMethodProperty(bean, name, nullBeansAllowed); } } diff --git a/server/tests/src/com/vaadin/data/util/BeanContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanContainerTest.java index 9037e303a8..2dcbb4aed8 100644 --- a/server/tests/src/com/vaadin/data/util/BeanContainerTest.java +++ b/server/tests/src/com/vaadin/data/util/BeanContainerTest.java @@ -457,4 +457,28 @@ public class BeanContainerTest extends AbstractBeanContainerTest { .getValue()); } + public void testNestedContainerPropertyWithNullBean() { + BeanContainer container = new BeanContainer( + NestedMethodPropertyTest.Person.class); + container.setBeanIdProperty("name"); + + container.addBean(new NestedMethodPropertyTest.Person("John", null)); + assertTrue(container + .addNestedContainerProperty("address.postalCodeObject")); + assertTrue(container.addNestedContainerProperty("address.street", true)); + // the nested properties added with allowNullBean setting should return + // null + assertNull(container.getContainerProperty("John", "address.street") + .getValue()); + // nested properties added without allowNullBean setting should throw + // exception + try { + container.getContainerProperty("John", "address.postalCodeObject") + .getValue(); + fail(); + } catch (Exception e) { + // should throw exception + } + } + } diff --git a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java index 6b88eb336d..3a2cb268b9 100644 --- a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java +++ b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java @@ -714,4 +714,27 @@ public class BeanItemContainerTest extends AbstractBeanContainerTest { .getValue()); } + public void testNestedContainerPropertyWithNullBean() { + BeanItemContainer container = new BeanItemContainer( + NestedMethodPropertyTest.Person.class); + NestedMethodPropertyTest.Person john = new NestedMethodPropertyTest.Person( + "John", null); + assertNotNull(container.addBean(john)); + assertTrue(container + .addNestedContainerProperty("address.postalCodeObject")); + assertTrue(container.addNestedContainerProperty("address.street", true)); + // the nested properties added with allowNullBean setting should return + // null + assertNull(container.getContainerProperty(john, "address.street") + .getValue()); + // nested properties added without allowNullBean setting should throw + // exception + try { + container.getContainerProperty(john, "address.postalCodeObject") + .getValue(); + fail(); + } catch (Exception e) { + // should throw exception + } + } } diff --git a/server/tests/src/com/vaadin/data/util/NestedMethodPropertyTest.java b/server/tests/src/com/vaadin/data/util/NestedMethodPropertyTest.java index 640ede8743..d517322010 100644 --- a/server/tests/src/com/vaadin/data/util/NestedMethodPropertyTest.java +++ b/server/tests/src/com/vaadin/data/util/NestedMethodPropertyTest.java @@ -273,6 +273,23 @@ public class NestedMethodPropertyTest extends TestCase { Assert.assertEquals("Joonas", managerNameProperty.getValue()); } + public void testNullNestedPropertyWithAllowNullBeans() { + NestedMethodProperty managerNameProperty = new NestedMethodProperty( + vaadin, "manager.name", true); + NestedMethodProperty streetProperty = new NestedMethodProperty( + vaadin, "manager.address.street", true); + + joonas.setAddress(null); + // should return null + Assert.assertNull(streetProperty.getValue()); + + vaadin.setManager(null); + Assert.assertNull(managerNameProperty.getValue()); + vaadin.setManager(joonas); + Assert.assertEquals("Joonas", managerNameProperty.getValue()); + Assert.assertNull(streetProperty.getValue()); + } + public void testMultiLevelNestedPropertySetValue() { NestedMethodProperty managerNameProperty = new NestedMethodProperty( vaadin, "manager.name"); @@ -314,6 +331,20 @@ public class NestedMethodPropertyTest extends TestCase { Assert.assertEquals("Ruukinkatu 2-4", property2.getValue()); } + public void testSerializationWithNullBeansAllowed() throws IOException, + ClassNotFoundException { + vaadin.setManager(null); + NestedMethodProperty streetProperty = new NestedMethodProperty( + vaadin, "manager.address.street", true); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new ObjectOutputStream(baos).writeObject(streetProperty); + @SuppressWarnings("unchecked") + NestedMethodProperty property2 = (NestedMethodProperty) new ObjectInputStream( + new ByteArrayInputStream(baos.toByteArray())).readObject(); + + Assert.assertNull(property2.getValue()); + } + public void testIsReadOnly() { NestedMethodProperty streetProperty = new NestedMethodProperty( vaadin, "manager.address.street"); diff --git a/server/tests/src/com/vaadin/data/util/PropertyDescriptorTest.java b/server/tests/src/com/vaadin/data/util/PropertyDescriptorTest.java index 14e70d76d4..0ae76430f6 100644 --- a/server/tests/src/com/vaadin/data/util/PropertyDescriptorTest.java +++ b/server/tests/src/com/vaadin/data/util/PropertyDescriptorTest.java @@ -52,4 +52,20 @@ public class PropertyDescriptorTest extends TestCase { Property property = pd2.createProperty(new Person("John", null)); Assert.assertEquals("John", property.getValue()); } + + public void testNestedPropertyDescriptorWithNullBeansAllowedSerialization() + throws Exception { + NestedPropertyDescriptor pd = new NestedPropertyDescriptor( + "address.street", Person.class, true); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new ObjectOutputStream(baos).writeObject(pd); + @SuppressWarnings("unchecked") + VaadinPropertyDescriptor pd2 = (VaadinPropertyDescriptor) new ObjectInputStream( + new ByteArrayInputStream(baos.toByteArray())).readObject(); + + Property property = pd2.createProperty(new Person("John", null)); + Assert.assertNull(property.getValue()); + } + } -- 2.39.5