SPR-6032 & SPR-6033: Auto grow nested path enhancements to BeanWrapper
git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@2030 50f2f4bb-b051-0410-bef5-90022cba6387
This commit is contained in:
parent
b987b115ca
commit
06201b5eb4
|
|
@ -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.
|
||||
* <p>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;
|
||||
|
|
|
|||
|
|
@ -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<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() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue