Fixed type resolution in case of inconsistencies between read and write method

Issue: SPR-11361
This commit is contained in:
Juergen Hoeller
2014-01-28 01:20:27 +01:00
parent 398f91cb74
commit ac3c670f70
3 changed files with 74 additions and 55 deletions

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -59,7 +59,7 @@ public interface BeanWrapper extends ConfigurablePropertyAccessor {
* @return the type of the wrapped bean instance, * @return the type of the wrapped bean instance,
* or {@code null} if no wrapped object has been set * or {@code null} if no wrapped object has been set
*/ */
Class getWrappedClass(); Class<?> getWrappedClass();
/** /**
* Obtain the PropertyDescriptors for the wrapped object * Obtain the PropertyDescriptors for the wrapped object
@@ -79,11 +79,13 @@ public interface BeanWrapper extends ConfigurablePropertyAccessor {
PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException; PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException;
/** /**
* Set whether this BeanWrapper should attempt to "auto-grow" a nested path that contains a null value. * Set whether this BeanWrapper should attempt to "auto-grow" a
* <p>If "true", a null path location will be populated with a default object value and traversed * nested path that contains a {@code null} value.
* instead of resulting in a {@link NullValueInNestedPathException}. Turning this flag on also * <p>If {@code true}, a {@code null} path location will be populated
* enables auto-growth of collection elements when accessing an out-of-bounds index. * with a default object value and traversed instead of resulting in a
* <p>Default is "false" on a plain BeanWrapper. * {@link NullValueInNestedPathException}. Turning this flag on also enables
* auto-growth of collection elements when accessing an out-of-bounds index.
* <p>Default is {@code false} on a plain BeanWrapper.
*/ */
void setAutoGrowNestedPaths(boolean autoGrowNestedPaths); void setAutoGrowNestedPaths(boolean autoGrowNestedPaths);

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -223,7 +223,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
return this.object; return this.object;
} }
public final Class getWrappedClass() { public final Class<?> getWrappedClass() {
return (this.object != null ? this.object.getClass() : null); return (this.object != null ? this.object.getClass() : null);
} }
@@ -246,7 +246,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
* Return the class of the root object at the top of the path of this BeanWrapper. * Return the class of the root object at the top of the path of this BeanWrapper.
* @see #getNestedPath * @see #getNestedPath
*/ */
public final Class getRootClass() { public final Class<?> getRootClass() {
return (this.rootObject != null ? this.rootObject.getClass() : null); return (this.rootObject != null ? this.rootObject.getClass() : null);
} }
@@ -304,7 +304,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
* Needs to be called when the target object changes. * Needs to be called when the target object changes.
* @param clazz the class to introspect * @param clazz the class to introspect
*/ */
protected void setIntrospectionClass(Class clazz) { protected void setIntrospectionClass(Class<?> clazz) {
if (this.cachedIntrospectionResults != null && if (this.cachedIntrospectionResults != null &&
!clazz.equals(this.cachedIntrospectionResults.getBeanClass())) { !clazz.equals(this.cachedIntrospectionResults.getBeanClass())) {
this.cachedIntrospectionResults = null; this.cachedIntrospectionResults = null;
@@ -352,7 +352,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
@Override @Override
public Class getPropertyType(String propertyName) throws BeansException { public Class<?> getPropertyType(String propertyName) throws BeansException {
try { try {
PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName); PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
if (pd != null) { if (pd != null) {
@@ -366,7 +366,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
// Check to see if there is a custom editor, // Check to see if there is a custom editor,
// which might give an indication on the desired target type. // which might give an indication on the desired target type.
Class editorType = guessPropertyTypeFromEditors(propertyName); Class<?> editorType = guessPropertyTypeFromEditors(propertyName);
if (editorType != null) { if (editorType != null) {
return editorType; return editorType;
} }
@@ -485,13 +485,13 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
"No property '" + propertyName + "' found"); "No property '" + propertyName + "' found");
} }
return convertForProperty(propertyName, null, value, pd); return convertForProperty(propertyName, null, value, new TypeDescriptor(property(pd)));
} }
private Object convertForProperty(String propertyName, Object oldValue, Object newValue, PropertyDescriptor pd) private Object convertForProperty(String propertyName, Object oldValue, Object newValue, TypeDescriptor td)
throws TypeMismatchException { throws TypeMismatchException {
return convertIfNecessary(propertyName, oldValue, newValue, pd.getPropertyType(), new TypeDescriptor(property(pd))); return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);
} }
private Property property(PropertyDescriptor pd) { private Property property(PropertyDescriptor pd) {
@@ -699,7 +699,8 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
return nestedBw.getPropertyValue(tokens); return nestedBw.getPropertyValue(tokens);
} }
private Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { @SuppressWarnings("unchecked")
private Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException {
String propertyName = tokens.canonicalName; String propertyName = tokens.canonicalName;
String actualName = tokens.actualName; String actualName = tokens.actualName;
PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName); PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
@@ -766,20 +767,20 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
else if (value instanceof List) { else if (value instanceof List) {
int index = Integer.parseInt(key); int index = Integer.parseInt(key);
List list = (List) value; List<Object> list = (List<Object>) value;
growCollectionIfNecessary(list, index, indexedPropertyName, pd, i + 1); growCollectionIfNecessary(list, index, indexedPropertyName, pd, i + 1);
value = list.get(index); 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.
Set set = (Set) value; Set<Object> set = (Set<Object>) value;
int index = Integer.parseInt(key); int index = Integer.parseInt(key);
if (index < 0 || index >= set.size()) { if (index < 0 || index >= set.size()) {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
"Cannot get element with index " + index + " from Set of size " + "Cannot get element with index " + index + " from Set of size " +
set.size() + ", accessed using property path '" + propertyName + "'"); set.size() + ", accessed using property path '" + propertyName + "'");
} }
Iterator it = set.iterator(); Iterator<Object> it = set.iterator();
for (int j = 0; it.hasNext(); j++) { for (int j = 0; it.hasNext(); j++) {
Object elem = it.next(); Object elem = it.next();
if (j == index) { if (j == index) {
@@ -789,11 +790,12 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
} }
else if (value instanceof Map) { else if (value instanceof Map) {
Map map = (Map) value; Map<Object, Object> map = (Map<Object, Object>) value;
Class<?> mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType(pd.getReadMethod(), i + 1); Class<?> mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType(pd.getReadMethod(), i + 1);
// IMPORTANT: Do not pass full property name in here - property editors // IMPORTANT: Do not pass full property name in here - property editors
// must not kick in for map keys but rather only for map values. // 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 convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
value = map.get(convertedMapKey); value = map.get(convertedMapKey);
} }
@@ -850,16 +852,15 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
} }
@SuppressWarnings("unchecked") private void growCollectionIfNecessary(Collection<Object> collection, int index, String name,
private void growCollectionIfNecessary( PropertyDescriptor pd, int nestingLevel) {
Collection collection, int index, String name, PropertyDescriptor pd, int nestingLevel) {
if (!this.autoGrowNestedPaths) { if (!this.autoGrowNestedPaths) {
return; return;
} }
int size = collection.size(); int size = collection.size();
if (index >= size && index < this.autoGrowCollectionLimit) { if (index >= size && index < this.autoGrowCollectionLimit) {
Class elementType = GenericCollectionTypeResolver.getCollectionReturnType(pd.getReadMethod(), nestingLevel); Class<?> elementType = GenericCollectionTypeResolver.getCollectionReturnType(pd.getReadMethod(), nestingLevel);
if (elementType != null) { if (elementType != null) {
for (int i = collection.size(); i < index + 1; i++) { for (int i = collection.size(); i < index + 1; i++) {
collection.add(newValue(elementType, name)); collection.add(newValue(elementType, name));
@@ -945,7 +946,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
if (propValue.getClass().isArray()) { if (propValue.getClass().isArray()) {
PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName); PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
Class requiredType = propValue.getClass().getComponentType(); Class<?> requiredType = propValue.getClass().getComponentType();
int arrayIndex = Integer.parseInt(key); int arrayIndex = Integer.parseInt(key);
Object oldValue = null; Object oldValue = null;
try { try {
@@ -963,9 +964,9 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
else if (propValue instanceof List) { else if (propValue instanceof List) {
PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName); PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
Class requiredType = GenericCollectionTypeResolver.getCollectionReturnType( Class<?> requiredType = GenericCollectionTypeResolver.getCollectionReturnType(
pd.getReadMethod(), tokens.keys.length); pd.getReadMethod(), tokens.keys.length);
List list = (List) propValue; List<Object> list = (List<Object>) propValue;
int index = Integer.parseInt(key); int index = Integer.parseInt(key);
Object oldValue = null; Object oldValue = null;
if (isExtractOldValueForEditor() && index < list.size()) { if (isExtractOldValueForEditor() && index < list.size()) {
@@ -1000,11 +1001,11 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
else if (propValue instanceof Map) { else if (propValue instanceof Map) {
PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName); PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
Class mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType( Class<?> mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType(
pd.getReadMethod(), tokens.keys.length); pd.getReadMethod(), tokens.keys.length);
Class mapValueType = GenericCollectionTypeResolver.getMapValueReturnType( Class<?> mapValueType = GenericCollectionTypeResolver.getMapValueReturnType(
pd.getReadMethod(), tokens.keys.length); pd.getReadMethod(), tokens.keys.length);
Map map = (Map) propValue; Map<Object, Object> map = (Map<Object, Object>) propValue;
// IMPORTANT: Do not pass full property name in here - property editors // IMPORTANT: Do not pass full property name in here - property editors
// must not kick in for map keys but rather only for map values. // must not kick in for map keys but rather only for map values.
TypeDescriptor typeDescriptor = (mapKeyType != null ? TypeDescriptor typeDescriptor = (mapKeyType != null ?
@@ -1094,7 +1095,8 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
} }
} }
} }
valueToApply = convertForProperty(propertyName, oldValue, originalValue, pd); valueToApply = convertForProperty(
propertyName, oldValue, originalValue, new TypeDescriptor(property(pd)));
} }
pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue); pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue);
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2013 the original author or authors. * Copyright 2002-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -16,13 +16,6 @@
package org.springframework.beans; package org.springframework.beans;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.beans.PropertyEditorSupport; import java.beans.PropertyEditorSupport;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
@@ -44,11 +37,16 @@ import java.util.TreeSet;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.beans.propertyeditors.CustomNumberEditor;
import org.springframework.beans.propertyeditors.StringArrayPropertyEditor; import org.springframework.beans.propertyeditors.StringArrayPropertyEditor;
import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.beans.support.DerivedFromProtectedBaseBean; import org.springframework.beans.support.DerivedFromProtectedBaseBean;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.tests.Assume; import org.springframework.tests.Assume;
import org.springframework.tests.TestGroup; import org.springframework.tests.TestGroup;
import org.springframework.tests.sample.beans.BooleanTestBean; import org.springframework.tests.sample.beans.BooleanTestBean;
@@ -56,15 +54,10 @@ import org.springframework.tests.sample.beans.ITestBean;
import org.springframework.tests.sample.beans.IndexedTestBean; import org.springframework.tests.sample.beans.IndexedTestBean;
import org.springframework.tests.sample.beans.NumberTestBean; import org.springframework.tests.sample.beans.NumberTestBean;
import org.springframework.tests.sample.beans.TestBean; import org.springframework.tests.sample.beans.TestBean;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.util.StopWatch; import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@@ -1557,17 +1550,15 @@ public final class BeanWrapperTests {
@Test @Test
public void cornerSpr10115() { public void cornerSpr10115() {
Spr10115Bean foo = new Spr10115Bean(); Spr10115Bean foo = new Spr10115Bean();
BeanWrapperImpl bwi = new BeanWrapperImpl(); BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
bwi.setWrappedInstance(foo);
bwi.setPropertyValue("prop1", "val1"); bwi.setPropertyValue("prop1", "val1");
assertEquals("val1", Spr10115Bean.prop1); assertEquals("val1", Spr10115Bean.prop1);
} }
@Test @Test
public void testArrayToObject() throws Exception { public void testArrayToObject() {
ArrayToObject foo = new ArrayToObject(); ArrayToObject foo = new ArrayToObject();
BeanWrapperImpl bwi = new BeanWrapperImpl(); BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
bwi.setWrappedInstance(foo);
Object[] array = new Object[] {"1","2"}; Object[] array = new Object[] {"1","2"};
bwi.setPropertyValue("object", array ); bwi.setPropertyValue("object", array );
@@ -1576,9 +1567,20 @@ public final class BeanWrapperTests {
array = new Object[] {"1"}; array = new Object[] {"1"};
bwi.setPropertyValue("object", array ); bwi.setPropertyValue("object", array );
assertThat(foo.getObject(), equalTo((Object) array)); assertThat(foo.getObject(), equalTo((Object) array));
} }
@Test
public void testPropertyTypeMismatch() {
PropertyTypeMismatch foo = new PropertyTypeMismatch();
BeanWrapperImpl bwi = new BeanWrapperImpl(foo);
bwi.setPropertyValue("object", "a String");
assertEquals("a String", foo.value);
assertEquals(8, bwi.getPropertyValue("object"));
}
static class Spr10115Bean { static class Spr10115Bean {
private static String prop1; private static String prop1;
public static void setProp1(String prop1) { public static void setProp1(String prop1) {
@@ -1963,11 +1965,10 @@ public final class BeanWrapperTests {
} }
static class ArrayToObject { public static class ArrayToObject {
private Object object; private Object object;
public void setObject(Object object) { public void setObject(Object object) {
this.object = object; this.object = object;
} }
@@ -1975,6 +1976,20 @@ public final class BeanWrapperTests {
public Object getObject() { public Object getObject() {
return object; return object;
} }
} }
public static class PropertyTypeMismatch {
public String value;
public void setObject(String object) {
this.value = object;
}
public Integer getObject() {
return (this.value != null ? this.value.length() : null);
}
}
} }