From 3fa533ddd85f8a33c11eda70387c347e2b4ce0ee Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Tue, 29 Sep 2009 19:54:35 +0000 Subject: [PATCH] SPR-6032 & SPR-6033: Auto grow nested path enhancements to BeanWrapper --- .../beans/BeanWrapperImpl.java | 131 ++++++++++- .../beans/BeanWrapperAutoGrowingTests.java | 205 ++++++++++++++++++ 2 files changed, 330 insertions(+), 6 deletions(-) create mode 100644 org.springframework.beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index 09e1941c93..00213a5c42 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -28,6 +28,7 @@ import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -37,6 +38,7 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.CollectionFactory; import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionException; @@ -112,6 +114,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra /** The security context used for invoking the property methods */ private AccessControlContext acc; + private boolean autoGrowNestedPaths; /** * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards. @@ -249,6 +252,25 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra return (this.rootObject != null ? this.rootObject.getClass() : null); } + /** + * If this BeanWrapper should attempt to "autogrow" a nested path that contains a null value. + * If true, a null path location will be populated with a default object value and traversed instead of resulting in a {@link NullValueInNestedPathException}. + * Turning this flag on also enables auto-growth of collection elements when an index that is out of bounds is accessed. + */ + public boolean getAutoGrowNestedPaths() { + return this.autoGrowNestedPaths; + } + + /** + * Sets if this BeanWrapper should attempt to "autogrow" a nested path that contains a null value. + * If true, a null path location will be populated with a default object value and traversed instead of resulting in a {@link NullValueInNestedPathException}. + * Turning this flag on also enables auto-growth of collection elements when an index that is out of bounds is accessed. + * Default is false. + */ + public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) { + this.autoGrowNestedPaths = autoGrowNestedPaths; + } + /** * Set the class to introspect. * Needs to be called when the target object changes. @@ -484,7 +506,11 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra String canonicalName = tokens.canonicalName; Object propertyValue = getPropertyValue(tokens); if (propertyValue == null) { - throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName); + if (autoGrowNestedPaths) { + propertyValue = setDefaultValue(tokens); + } else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName); + } } // Lookup cached sub-BeanWrapper, create new one if not found. @@ -507,6 +533,51 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra return nestedBw; } + private Object setDefaultValue(String propertyName) { + PropertyTokenHolder tokens = new PropertyTokenHolder(); + tokens.actualName = propertyName; + tokens.canonicalName = propertyName; + setPropertyValue(tokens, createDefaultPropertyValue(tokens)); + return getPropertyValue(tokens); + } + + private Object setDefaultValue(PropertyTokenHolder tokens) { + setPropertyValue(tokens, createDefaultPropertyValue(tokens)); + return getPropertyValue(tokens); + } + + private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { + PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(tokens.actualName); + Object defaultValue = newValue(pd.getPropertyType(), tokens.canonicalName); + return new PropertyValue(tokens.canonicalName, defaultValue); + } + + private Object newValue(Class type, String name) { + try { + if (type.isArray()) { + Class componentType = type.getComponentType(); + // TODO - only handles 2-dimensional arrays + if (componentType.isArray()) { + Object array = Array.newInstance(componentType, 1); + Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0)); + return array; + } else { + return Array.newInstance(componentType, 0); + } + } else { + if (Collection.class.isAssignableFrom(type)) { + return CollectionFactory.createCollection(type, 16); + } else { + return type.newInstance(); + } + } + } catch (InstantiationException e) { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, "Could not instantiate propertyType [" + type.getName() + "] to auto-grow nestd property path"); + } catch (IllegalAccessException e) { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, "Could not instantiate propertyType [" + type.getName() + "] to auto-grow nested property path"); + } + } + /** * Create a new nested BeanWrapper instance. *

Default implementation creates a BeanWrapperImpl instance. @@ -611,22 +682,36 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra else { value = readMethod.invoke(object, (Object[]) null); } - - if (tokens.keys != null) { + + if (tokens.keys != null) { + if (value == null) { + if (autoGrowNestedPaths) { + value = setDefaultValue(tokens.actualName); + } else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, + "Cannot access indexed value of property referenced in indexed " + + "property path '" + propertyName + "': returned null"); + } + } + String indexedPropertyName = tokens.actualName; // apply indexes and map keys for (int i = 0; i < tokens.keys.length; i++) { String key = tokens.keys[i]; if (value == null) { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, "Cannot access indexed value of property referenced in indexed " + - "property path '" + propertyName + "': returned null"); + "property path '" + propertyName + "': returned null"); } else if (value.getClass().isArray()) { - value = Array.get(value, Integer.parseInt(key)); + int index = Integer.parseInt(key); + value = growArrayIfNecessary(value, index, indexedPropertyName); + value = Array.get(value, index); } else if (value instanceof List) { + int index = Integer.parseInt(key); List list = (List) value; - value = list.get(Integer.parseInt(key)); + growCollectionIfNecessary(list, index, indexedPropertyName, pd, i + 1); + value = list.get(index); } else if (value instanceof Set) { // Apply index to Iterator in case of a Set. @@ -661,6 +746,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra "Property referenced in indexed property path '" + propertyName + "' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]"); } + indexedPropertyName += PROPERTY_KEY_PREFIX + key + PROPERTY_KEY_SUFFIX; } } return value; @@ -688,6 +774,39 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra } } + private Object growArrayIfNecessary(Object array, int index, String name) { + if (!autoGrowNestedPaths) { + return array; + } + int length = Array.getLength(array); + if (index >= length) { + Class componentType = array.getClass().getComponentType(); + Object newArray = Array.newInstance(componentType, index + 1); + System.arraycopy(array, 0, newArray, 0, length); + for (int i = length; i < Array.getLength(newArray); i++) { + Array.set(newArray, i, newValue(componentType, name)); + } + setPropertyValue(name, newArray); + return newArray; + } else { + return array; + } + } + + private void growCollectionIfNecessary(Collection collection, int index, String name, PropertyDescriptor pd, int nestingLevel) { + if (!autoGrowNestedPaths) { + return; + } + if (index >= collection.size()) { + Class elementType = GenericCollectionTypeResolver.getCollectionReturnType(pd.getReadMethod(), nestingLevel); + if (elementType != null) { + for (int i = collection.size(); i < index + 1; i++) { + collection.add(newValue(elementType, name)); + } + } + } + } + @Override public void setPropertyValue(String propertyName, Object value) throws BeansException { BeanWrapperImpl nestedBw; diff --git a/org.springframework.beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java b/org.springframework.beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java new file mode 100644 index 0000000000..21adde9373 --- /dev/null +++ b/org.springframework.beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java @@ -0,0 +1,205 @@ +package org.springframework.beans; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +public class BeanWrapperAutoGrowingTests { + + Bean bean = new Bean(); + + BeanWrapperImpl wrapper = new BeanWrapperImpl(bean); + + @Before + public void setUp() { + wrapper.setAutoGrowNestedPaths(true); + } + + @Test + public void getPropertyValueNullValueInNestedPath() { + assertNull(wrapper.getPropertyValue("nested.prop")); + } + + @Test + public void setPropertyValueNullValueInNestedPath() { + wrapper.setPropertyValue("nested.prop", "test"); + assertEquals("test", bean.getNested().getProp()); + } + + @Test(expected=NullValueInNestedPathException.class) + public void getPropertyValueNullValueInNestedPathNoDefaultConstructor() { + wrapper.getPropertyValue("nestedNoConstructor.prop"); + } + + @Test + public void getPropertyValueAutoGrowArray() { + assertNotNull(wrapper.getPropertyValue("array[0]")); + assertEquals(1, bean.getArray().length); + assertTrue(bean.getArray()[0] instanceof Bean); + } + + @Test + public void setPropertyValueAutoGrowArray() { + wrapper.setPropertyValue("array[0].prop", "test"); + assertEquals("test", bean.getArray()[0].getProp()); + } + + @Test + public void getPropertyValueAutoGrowArrayBySeveralElements() { + assertNotNull(wrapper.getPropertyValue("array[4]")); + assertEquals(5, bean.getArray().length); + assertTrue(bean.getArray()[0] instanceof Bean); + assertTrue(bean.getArray()[1] instanceof Bean); + assertTrue(bean.getArray()[2] instanceof Bean); + assertTrue(bean.getArray()[3] instanceof Bean); + assertTrue(bean.getArray()[4] instanceof Bean); + assertNotNull(wrapper.getPropertyValue("array[0]")); + assertNotNull(wrapper.getPropertyValue("array[1]")); + assertNotNull(wrapper.getPropertyValue("array[2]")); + assertNotNull(wrapper.getPropertyValue("array[3]")); + } + + @Test + public void getPropertyValueAutoGrowMultiDimensionalArray() { + assertNotNull(wrapper.getPropertyValue("multiArray[0][0]")); + assertEquals(1, bean.getMultiArray()[0].length); + assertTrue(bean.getMultiArray()[0][0] instanceof Bean); + } + + @Test + public void getPropertyValueAutoGrowList() { + assertNotNull(wrapper.getPropertyValue("list[0]")); + assertEquals(1, bean.getList().size()); + assertTrue(bean.getList().get(0) instanceof Bean); + } + + @Test + public void setPropertyValueAutoGrowList() { + wrapper.setPropertyValue("list[0].prop", "test"); + assertEquals("test", bean.getList().get(0).getProp()); + } + + @Test + public void getPropertyValueAutoGrowListBySeveralElements() { + assertNotNull(wrapper.getPropertyValue("list[4]")); + assertEquals(5, bean.getList().size()); + assertTrue(bean.getList().get(0) instanceof Bean); + assertTrue(bean.getList().get(1) instanceof Bean); + assertTrue(bean.getList().get(2) instanceof Bean); + assertTrue(bean.getList().get(3) instanceof Bean); + assertTrue(bean.getList().get(4) instanceof Bean); + assertNotNull(wrapper.getPropertyValue("list[0]")); + assertNotNull(wrapper.getPropertyValue("list[1]")); + assertNotNull(wrapper.getPropertyValue("list[2]")); + assertNotNull(wrapper.getPropertyValue("list[3]")); + } + + @Test + public void getPropertyValueAutoGrowMultiDimensionalList() { + assertNotNull(wrapper.getPropertyValue("multiList[0][0]")); + assertEquals(1, bean.getMultiList().get(0).size()); + assertTrue(bean.getMultiList().get(0).get(0) instanceof Bean); + } + + @Test(expected=InvalidPropertyException.class) + public void getPropertyValueAutoGrowListNotParameterized() { + wrapper.getPropertyValue("listNotParameterized[0]"); + } + + public static class Bean { + + private String prop; + + private Bean nested; + + private NestedNoDefaultConstructor nestedNoConstructor; + + private Bean[] array; + + private Bean[][] multiArray; + + private List list; + + private List> multiList; + + private List listNotParameterized; + + public String getProp() { + return prop; + } + + public void setProp(String prop) { + this.prop = prop; + } + + public Bean getNested() { + return nested; + } + + public void setNested(Bean nested) { + this.nested = nested; + } + + public Bean[] getArray() { + return array; + } + + public void setArray(Bean[] array) { + this.array = array; + } + + public Bean[][] getMultiArray() { + return multiArray; + } + + public void setMultiArray(Bean[][] multiArray) { + this.multiArray = multiArray; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public List> getMultiList() { + return multiList; + } + + public void setMultiList(List> multiList) { + this.multiList = multiList; + } + + public NestedNoDefaultConstructor getNestedNoConstructor() { + return nestedNoConstructor; + } + + public void setNestedNoConstructor( + NestedNoDefaultConstructor nestedNoConstructor) { + this.nestedNoConstructor = nestedNoConstructor; + } + + public List getListNotParameterized() { + return listNotParameterized; + } + + public void setListNotParameterized(List listNotParameterized) { + this.listNotParameterized = listNotParameterized; + } + + } + + public static class NestedNoDefaultConstructor { + private NestedNoDefaultConstructor() { + + } + } +}