restored DataBinder's ability to bind to an auto-growing List with unknown element type (SPR-8828)

This commit is contained in:
Juergen Hoeller
2011-12-07 21:27:26 +00:00
parent f9144ea1ea
commit 569426dfdf
8 changed files with 207 additions and 66 deletions

View File

@@ -512,11 +512,15 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
private Object convertForProperty(String propertyName, Object oldValue, Object newValue, PropertyDescriptor pd)
throws TypeMismatchException {
GenericTypeAwarePropertyDescriptor gpd = (GenericTypeAwarePropertyDescriptor) pd;
Class<?> beanClass = gpd.getBeanClass();
return convertIfNecessary(propertyName, oldValue, newValue, pd.getPropertyType(), new TypeDescriptor(property(pd)));
}
private Property property(PropertyDescriptor pd) {
GenericTypeAwarePropertyDescriptor typeAware = (GenericTypeAwarePropertyDescriptor) pd;
return new Property(typeAware.getBeanClass(), typeAware.getReadMethod(), typeAware.getWriteMethod());
}
//---------------------------------------------------------------------
// Implementation methods
@@ -970,7 +974,8 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) {
oldValue = Array.get(propValue, arrayIndex);
}
Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(), requiredType, TypeDescriptor.nested(property(pd), tokens.keys.length));
Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(),
requiredType, TypeDescriptor.nested(property(pd), tokens.keys.length));
Array.set(propValue, arrayIndex, convertedValue);
}
catch (IndexOutOfBoundsException ex) {
@@ -984,12 +989,13 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
pd.getReadMethod(), tokens.keys.length);
List list = (List) propValue;
int index = Integer.parseInt(key);
int size = list.size();
Object oldValue = null;
if (isExtractOldValueForEditor() && index < size) {
if (isExtractOldValueForEditor() && index < list.size()) {
oldValue = list.get(index);
}
Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(), requiredType, TypeDescriptor.nested(property(pd), tokens.keys.length));
Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(),
requiredType, TypeDescriptor.nested(property(pd), tokens.keys.length));
int size = list.size();
if (index >= size && index < this.autoGrowCollectionLimit) {
for (int i = size; i < index; i++) {
try {
@@ -1023,7 +1029,8 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
Map map = (Map) propValue;
// IMPORTANT: Do not pass full property name in here - property editors
// must not kick in for map keys but rather only for map values.
TypeDescriptor typeDescriptor = mapKeyType != null ? TypeDescriptor.valueOf(mapKeyType) : TypeDescriptor.valueOf(Object.class);
TypeDescriptor typeDescriptor = (mapKeyType != null ?
TypeDescriptor.valueOf(mapKeyType) : TypeDescriptor.valueOf(Object.class));
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
Object oldValue = null;
if (isExtractOldValueForEditor()) {
@@ -1031,8 +1038,8 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
}
// Pass full property name and old value in here, since we want full
// conversion ability for map values.
Object convertedMapValue = convertIfNecessary(
propertyName, oldValue, pv.getValue(), mapValueType, TypeDescriptor.nested(property(pd), tokens.keys.length));
Object convertedMapValue = convertIfNecessary(propertyName, oldValue, pv.getValue(),
mapValueType, TypeDescriptor.nested(property(pd), tokens.keys.length));
map.put(convertedMapKey, convertedMapValue);
}
else {
@@ -1194,10 +1201,5 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
public String[] keys;
}
private Property property(PropertyDescriptor pd) {
GenericTypeAwarePropertyDescriptor typeAware = (GenericTypeAwarePropertyDescriptor) pd;
return new Property(typeAware.getBeanClass(), typeAware.getReadMethod(), typeAware.getWriteMethod());
}
}

View File

@@ -93,7 +93,7 @@ class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor {
}
public Class<?> getBeanClass() {
return beanClass;
return this.beanClass;
}
@Override

View File

@@ -22,7 +22,13 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -45,6 +51,7 @@ import org.springframework.beans.propertyeditors.CustomNumberEditor;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.number.NumberFormatter;
import org.springframework.format.support.FormattingConversionService;
@@ -1552,6 +1559,20 @@ public class DataBinderTests extends TestCase {
}
}
public void testNestedGrowingList() {
Form form = new Form();
DataBinder binder = new DataBinder(form, "form");
MutablePropertyValues mpv = new MutablePropertyValues();
mpv.add("f[list][0]", "firstValue");
mpv.add("f[list][1]", "secondValue");
binder.bind(mpv);
assertFalse(binder.getBindingResult().hasErrors());
List<Object> list = (List<Object>) form.getF().get("list");
assertEquals("firstValue", list.get(0));
assertEquals("secondValue", list.get(1));
assertEquals(2, list.size());
}
private static class BeanWithIntegerList {
@@ -1646,4 +1667,94 @@ public class DataBinderTests extends TestCase {
}
}
private static class GrowingList<E> extends AbstractList<E> {
private List<E> list;
public GrowingList() {
this.list = new ArrayList<E>();
}
public List<E> getWrappedList() {
return list;
}
public E get(int index) {
if (index >= list.size()) {
for (int i = list.size(); i < index; i++) {
list.add(null);
}
list.add(null);
return null;
}
else {
return list.get(index);
}
}
public int size() {
return list.size();
}
public boolean add(E o) {
return list.add(o);
}
public void add(int index, E element) {
list.add(index, element);
}
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(index, c);
}
public void clear() {
list.clear();
}
public int indexOf(Object o) {
return list.indexOf(o);
}
public Iterator<E> iterator() {
return list.iterator();
}
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
public ListIterator<E> listIterator() {
return list.listIterator();
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}
public E remove(int index) {
return list.remove(index);
}
public E set(int index, E element) {
return list.set(index, element);
}
}
private static class Form {
private final Map<String, Object> f;
public Form() {
f = new HashMap<String, Object>();
f.put("list", new GrowingList<Object>());
}
public Map<String, Object> getF() {
return f;
}
}
}

View File

@@ -20,6 +20,8 @@ import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Map;
import org.springframework.util.Assert;
/**
* @author Keith Donald
* @since 3.1
@@ -28,15 +30,15 @@ abstract class AbstractDescriptor {
private final Class<?> type;
protected AbstractDescriptor(Class<?> type) {
if (type == null) {
throw new IllegalArgumentException("type cannot be null");
}
Assert.notNull(type, "Type must not be null");
this.type = type;
}
public Class<?> getType() {
return type;
return this.type;
}
public TypeDescriptor getElementTypeDescriptor() {
@@ -73,27 +75,33 @@ abstract class AbstractDescriptor {
}
}
public abstract Annotation[] getAnnotations();
public AbstractDescriptor nested() {
if (isCollection()) {
Class<?> elementType = resolveCollectionElementType();
return elementType != null ? nested(elementType, 0) : null;
return (elementType != null ? nested(elementType, 0) : null);
}
else if (isArray()) {
return nested(getType().getComponentType(), 0);
}
else if (isMap()) {
Class<?> mapValueType = resolveMapValueType();
return mapValueType != null ? nested(mapValueType, 1) : null;
return (mapValueType != null ? nested(mapValueType, 1) : null);
}
else if (Object.class.equals(getType())) {
// could be a collection type but we don't know about its element type,
// so let's just assume there is an element type of type Object
return this;
}
else {
throw new IllegalStateException("Not a collection, array, or map: cannot resolve nested value types");
}
}
// subclassing hooks
public abstract Annotation[] getAnnotations();
protected abstract Class<?> resolveCollectionElementType();
protected abstract Class<?> resolveMapKeyType();
@@ -102,6 +110,7 @@ abstract class AbstractDescriptor {
protected abstract AbstractDescriptor nested(Class<?> type, int typeIndex);
// internal helpers
private boolean isCollection() {

View File

@@ -20,6 +20,10 @@ import java.lang.annotation.Annotation;
import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.core.MethodParameter;
/**
* @author Keith Donald
* @since 3.1
*/
class BeanPropertyDescriptor extends AbstractDescriptor {
private final Property property;
@@ -28,6 +32,7 @@ class BeanPropertyDescriptor extends AbstractDescriptor {
private final Annotation[] annotations;
public BeanPropertyDescriptor(Property property) {
super(property.getType());
this.property = property;
@@ -35,24 +40,25 @@ class BeanPropertyDescriptor extends AbstractDescriptor {
this.annotations = property.getAnnotations();
}
@Override
public Annotation[] getAnnotations() {
return annotations;
return this.annotations;
}
@Override
protected Class<?> resolveCollectionElementType() {
return GenericCollectionTypeResolver.getCollectionParameterType(methodParameter);
return GenericCollectionTypeResolver.getCollectionParameterType(this.methodParameter);
}
@Override
protected Class<?> resolveMapKeyType() {
return GenericCollectionTypeResolver.getMapKeyParameterType(methodParameter);
return GenericCollectionTypeResolver.getMapKeyParameterType(this.methodParameter);
}
@Override
protected Class<?> resolveMapValueType() {
return GenericCollectionTypeResolver.getMapValueParameterType(methodParameter);
return GenericCollectionTypeResolver.getMapValueParameterType(this.methodParameter);
}
@Override
@@ -60,9 +66,10 @@ class BeanPropertyDescriptor extends AbstractDescriptor {
MethodParameter methodParameter = new MethodParameter(this.methodParameter);
methodParameter.increaseNestingLevel();
methodParameter.setTypeIndexForCurrentLevel(typeIndex);
return new BeanPropertyDescriptor(type, property, methodParameter, annotations);
return new BeanPropertyDescriptor(type, this.property, methodParameter, this.annotations);
}
// internal
private BeanPropertyDescriptor(Class<?> type, Property propertyDescriptor, MethodParameter methodParameter, Annotation[] annotations) {
@@ -72,4 +79,4 @@ class BeanPropertyDescriptor extends AbstractDescriptor {
this.annotations = annotations;
}
}
}

View File

@@ -53,7 +53,7 @@ class FieldDescriptor extends AbstractDescriptor {
@Override
public Annotation[] getAnnotations() {
return TypeDescriptor.nullSafeAnnotations(field.getAnnotations());
return TypeDescriptor.nullSafeAnnotations(this.field.getAnnotations());
}
@Override

View File

@@ -27,11 +27,16 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* A description of a JavaBeans Property that allows us to avoid a dependency on java.beans.PropertyDescriptor.
* java.beans is not available in a number of environments (e.g. Android, Java ME), so this is desirable.
* Used to build a TypeDescriptor from a property location.
* A description of a JavaBeans Property that allows us to avoid a dependency on
* <code>java.beans.PropertyDescriptor</code>. The <code>java.beans</code> package
* is not available in a number of environments (e.g. Android, Java ME), so this is
* desirable for portability of Spring's core conversion facility.
*
* <p>Used to build a TypeDescriptor from a property location.
* The built TypeDescriptor can then be used to convert from/to the property type.
*
* @author Keith Donald
* @since 3.1
* @see TypeDescriptor#TypeDescriptor(Property)
* @see TypeDescriptor#nested(Property, int)
*/
@@ -49,6 +54,7 @@ public final class Property {
private final Annotation[] annotations;
public Property(Class<?> objectType, Method readMethod, Method writeMethod) {
this.objectType = objectType;
this.readMethod = readMethod;
@@ -58,72 +64,77 @@ public final class Property {
this.annotations = resolveAnnotations();
}
/**
* The object declaring this property, either directly or in a superclass the object extends.
*/
public Class<?> getObjectType() {
return objectType;
return this.objectType;
}
/**
* The name of the property e.g. 'foo'.
* The name of the property: e.g. 'foo'
*/
public String getName() {
return name;
return this.name;
}
/**
* The property type e.g. java.lang.String.
* The property type: e.g. <code>java.lang.String</code>
*/
public Class<?> getType() {
return methodParameter.getParameterType();
return this.methodParameter.getParameterType();
}
/**
* The property getter method e.g. getFoo()
* The property getter method: e.g. <code>getFoo()</code>
*/
public Method getReadMethod() {
return readMethod;
return this.readMethod;
}
/**
* The property setter method e.g. setFoo(String).
* The property setter method: e.g. <code>setFoo(String)</code>
*/
public Method getWriteMethod() {
return writeMethod;
return this.writeMethod;
}
// package private
MethodParameter getMethodParameter() {
return methodParameter;
return this.methodParameter;
}
Annotation[] getAnnotations() {
return annotations;
return this.annotations;
}
// internal helpers
private String resolveName() {
if (readMethod != null) {
int index = readMethod.getName().indexOf("get");
if (this.readMethod != null) {
int index = this.readMethod.getName().indexOf("get");
if (index != -1) {
index += 3;
} else {
index = readMethod.getName().indexOf("is");
}
else {
index = this.readMethod.getName().indexOf("is");
if (index == -1) {
throw new IllegalArgumentException("Not a getter method");
}
index += 2;
}
return StringUtils.uncapitalize(readMethod.getName().substring(index));
} else {
int index = writeMethod.getName().indexOf("set") + 3;
return StringUtils.uncapitalize(this.readMethod.getName().substring(index));
}
else {
int index = this.writeMethod.getName().indexOf("set") + 3;
if (index == -1) {
throw new IllegalArgumentException("Not a setter method");
}
return StringUtils.uncapitalize(writeMethod.getName().substring(index));
return StringUtils.uncapitalize(this.writeMethod.getName().substring(index));
}
}
@@ -204,7 +215,8 @@ public final class Property {
private Class<?> declaringClass() {
if (getReadMethod() != null) {
return getReadMethod().getDeclaringClass();
} else {
}
else {
return getWriteMethod().getDeclaringClass();
}
}

View File

@@ -164,12 +164,12 @@ public class TypeDescriptor {
/**
* Creates a type descriptor for a nested type declared within the field.
* For example, if the field is a List&lt;String&gt; and the nestingLevel is 1, the nested type descriptor will be String.class.
* If the field is a List<List<String>> and the nestingLevel is 2, the nested type descriptor will also be a String.class.
* If the field is a Map<Integer, String> and the nestingLevel is 1, the nested type descriptor will be String, derived from the map value.
* If the field is a List<Map<Integer, String>> and the nestingLevel is 2, the nested type descriptor will be String, derived from the map value.
* Returns null if a nested type cannot be obtained because it was not declared.
* For example, if the field is a List&lt;?&gt;, the nested type descriptor returned will be null.
* <p>For example, if the field is a <code>List&lt;String&gt;</code> and the nestingLevel is 1, the nested type descriptor will be <code>String.class</code>.
* If the field is a <code>List&lt;List&lt;String&gt;&gt;</code> and the nestingLevel is 2, the nested type descriptor will also be a <code>String.class</code>.
* If the field is a <code>Map&lt;Integer, String&gt;</code> and the nestingLevel is 1, the nested type descriptor will be String, derived from the map value.
* If the field is a <code>List&lt;Map&lt;Integer, String&gt;&gt;</code> and the nestingLevel is 2, the nested type descriptor will be String, derived from the map value.
* Returns <code>null</code> if a nested type cannot be obtained because it was not declared.
* For example, if the field is a <code>List&lt;?&gt;</code>, the nested type descriptor returned will be <code>null</code>.
* @param field the field
* @param nestingLevel the nesting level of the collection/array element or map key/value declaration within the field.
* @return the nested type descriptor at the specified nestingLevel, or null if it could not be obtained
@@ -181,15 +181,15 @@ public class TypeDescriptor {
/**
* Creates a type descriptor for a nested type declared within the property.
* For example, if the property is a List&lt;String&gt; and the nestingLevel is 1, the nested type descriptor will be String.class.
* If the property is a List<List<String>> and the nestingLevel is 2, the nested type descriptor will also be a String.class.
* If the field is a Map<Integer, String> and the nestingLevel is 1, the nested type descriptor will be String, derived from the map value.
* If the property is a List<Map<Integer, String>> and the nestingLevel is 2, the nested type descriptor will be String, derived from the map value.
* Returns null if a nested type cannot be obtained because it was not declared.
* For example, if the property is a List&lt;?&gt;, the nested type descriptor returned will be null.
* <p>For example, if the property is a <code>List&lt;String&gt;</code> and the nestingLevel is 1, the nested type descriptor will be <code>String.class</code>.
* If the property is a <code>List&lt;List&lt;String&gt;&gt;</code> and the nestingLevel is 2, the nested type descriptor will also be a <code>String.class</code>.
* If the property is a <code>Map&lt;Integer, String&gt;</code> and the nestingLevel is 1, the nested type descriptor will be String, derived from the map value.
* If the property is a <code>List&lt;Map&lt;Integer, String&gt;&gt;</code> and the nestingLevel is 2, the nested type descriptor will be String, derived from the map value.
* Returns <code>null</code> if a nested type cannot be obtained because it was not declared.
* For example, if the property is a <code>List&lt;?&gt;</code>, the nested type descriptor returned will be <code>null</code>.
* @param property the property
* @param nestingLevel the nesting level of the collection/array element or map key/value declaration within the property.
* @return the nested type descriptor at the specified nestingLevel, or null if it could not be obtained
* @return the nested type descriptor at the specified nestingLevel, or <code>null</code> if it could not be obtained
* @throws IllegalArgumentException if the types up to the specified nesting level are not of collection, array, or map types.
*/
public static TypeDescriptor nested(Property property, int nestingLevel) {