提交 3fa533dd 编写于 作者: K Keith Donald

SPR-6032 & SPR-6033: Auto grow nested path enhancements to BeanWrapper

上级 832db0ab
...@@ -28,6 +28,7 @@ import java.security.PrivilegedAction; ...@@ -28,6 +28,7 @@ import java.security.PrivilegedAction;
import java.security.PrivilegedActionException; import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
...@@ -37,6 +38,7 @@ import java.util.Set; ...@@ -37,6 +38,7 @@ import java.util.Set;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.core.CollectionFactory;
import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionException;
...@@ -112,6 +114,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra ...@@ -112,6 +114,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
/** The security context used for invoking the property methods */ /** The security context used for invoking the property methods */
private AccessControlContext acc; private AccessControlContext acc;
private boolean autoGrowNestedPaths;
/** /**
* Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards. * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards.
...@@ -249,6 +252,25 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra ...@@ -249,6 +252,25 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
return (this.rootObject != null ? this.rootObject.getClass() : null); 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. * Set the class to introspect.
* Needs to be called when the target object changes. * Needs to be called when the target object changes.
...@@ -484,7 +506,11 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra ...@@ -484,7 +506,11 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
String canonicalName = tokens.canonicalName; String canonicalName = tokens.canonicalName;
Object propertyValue = getPropertyValue(tokens); Object propertyValue = getPropertyValue(tokens);
if (propertyValue == null) { 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. // Lookup cached sub-BeanWrapper, create new one if not found.
...@@ -507,6 +533,51 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra ...@@ -507,6 +533,51 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
return nestedBw; 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. * Create a new nested BeanWrapper instance.
* <p>Default implementation creates a BeanWrapperImpl instance. * <p>Default implementation creates a BeanWrapperImpl instance.
...@@ -611,22 +682,36 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra ...@@ -611,22 +682,36 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
else { else {
value = readMethod.invoke(object, (Object[]) null); 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 // apply indexes and map keys
for (int i = 0; i < tokens.keys.length; i++) { for (int i = 0; i < tokens.keys.length; i++) {
String key = tokens.keys[i]; String key = tokens.keys[i];
if (value == null) { if (value == null) {
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
"Cannot access indexed value of property referenced in indexed " + "Cannot access indexed value of property referenced in indexed " +
"property path '" + propertyName + "': returned null"); "property path '" + propertyName + "': returned null");
} }
else if (value.getClass().isArray()) { 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) { else if (value instanceof List) {
int index = Integer.parseInt(key);
List list = (List) value; 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) { else if (value instanceof Set) {
// Apply index to Iterator in case of a Set. // Apply index to Iterator in case of a Set.
...@@ -661,6 +746,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra ...@@ -661,6 +746,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
"Property referenced in indexed property path '" + propertyName + "Property referenced in indexed property path '" + propertyName +
"' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]"); "' 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; return value;
...@@ -688,6 +774,39 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra ...@@ -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 @Override
public void setPropertyValue(String propertyName, Object value) throws BeansException { public void setPropertyValue(String propertyName, Object value) throws BeansException {
BeanWrapperImpl nestedBw; BeanWrapperImpl nestedBw;
......
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<Bean> list;
private List<List<Bean>> 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<Bean> getList() {
return list;
}
public void setList(List<Bean> list) {
this.list = list;
}
public List<List<Bean>> getMultiList() {
return multiList;
}
public void setMultiList(List<List<Bean>> 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() {
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册