SPR-6032 & SPR-6033: Auto grow nested path enhancements to BeanWrapper
This commit is contained in:
@@ -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;
|
|||||||
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
|
|||||||
/** 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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
"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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user