diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java index 7fea8958fe5..078712179a1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java @@ -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"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import java.util.Map; * implementation of actual property access left to subclasses. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.0 * @see #getPropertyValue * @see #setPropertyValue @@ -35,6 +36,8 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl private boolean extractOldValueForEditor = false; + private boolean autoGrowNestedPaths = false; + @Override public void setExtractOldValueForEditor(boolean extractOldValueForEditor) { @@ -46,6 +49,16 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl return this.extractOldValueForEditor; } + @Override + public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) { + this.autoGrowNestedPaths = autoGrowNestedPaths; + } + + @Override + public boolean isAutoGrowNestedPaths() { + return this.autoGrowNestedPaths; + } + @Override public void setPropertyValue(PropertyValue pv) throws BeansException { diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java index 16992e2e16b..7131651c6e1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java @@ -78,22 +78,6 @@ public interface BeanWrapper extends ConfigurablePropertyAccessor { */ PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException; - /** - * Set whether this BeanWrapper should attempt to "auto-grow" a - * nested path that contains a {@code null} value. - *

If {@code true}, a {@code 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 accessing an out-of-bounds index. - *

Default is {@code false} on a plain BeanWrapper. - */ - void setAutoGrowNestedPaths(boolean autoGrowNestedPaths); - - /** - * Return whether "auto-growing" of nested paths has been activated. - */ - boolean isAutoGrowNestedPaths(); - /** * Specify a limit for array and collection auto-growing. *

Default is unlimited on a plain BeanWrapper. diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index d59e89a42c5..e43c9013e55 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -115,8 +115,6 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra */ private Map nestedBeanWrappers; - private boolean autoGrowNestedPaths = false; - private int autoGrowCollectionLimit = Integer.MAX_VALUE; @@ -252,25 +250,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra return (this.rootObject != null ? this.rootObject.getClass() : null); } - /** - * Set whether this BeanWrapper should attempt to "auto-grow" 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 accessing an out-of-bounds index. - *

Default is "false" on a plain BeanWrapper. - */ - @Override - public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) { - this.autoGrowNestedPaths = autoGrowNestedPaths; - } - /** - * Return whether "auto-growing" of nested paths has been activated. - */ - @Override - public boolean isAutoGrowNestedPaths() { - return this.autoGrowNestedPaths; - } /** * Specify a limit for array and collection auto-growing. @@ -570,7 +550,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra String canonicalName = tokens.canonicalName; Object propertyValue = getPropertyValue(tokens); if (propertyValue == null) { - if (this.autoGrowNestedPaths) { + if (isAutoGrowNestedPaths()) { propertyValue = setDefaultValue(tokens); } else { @@ -761,7 +741,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra if (tokens.keys != null) { if (value == null) { - if (this.autoGrowNestedPaths) { + if (isAutoGrowNestedPaths()) { value = setDefaultValue(tokens.actualName); } else { @@ -851,7 +831,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra } private Object growArrayIfNecessary(Object array, int index, String name) { - if (!this.autoGrowNestedPaths) { + if (!isAutoGrowNestedPaths()) { return array; } int length = Array.getLength(array); @@ -874,7 +854,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra private void growCollectionIfNecessary(Collection collection, int index, String name, PropertyDescriptor pd, int nestingLevel) { - if (!this.autoGrowNestedPaths) { + if (!isAutoGrowNestedPaths()) { return; } int size = collection.size(); @@ -951,7 +931,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra String key = tokens.keys[tokens.keys.length - 1]; if (propValue == null) { // null map value case - if (this.autoGrowNestedPaths) { + if (isAutoGrowNestedPaths()) { // TODO: cleanup, this is pretty hacky int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex); diff --git a/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java index 54dd62cf53a..553733ab8c2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.core.convert.ConversionService; *

Serves as base interface for {@link BeanWrapper}. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.0 * @see BeanWrapper */ @@ -54,4 +55,19 @@ public interface ConfigurablePropertyAccessor extends PropertyAccessor, Property */ boolean isExtractOldValueForEditor(); + /** + * Set whether this instance should attempt to "auto-grow" a + * nested path that contains a {@code null} value. + *

If {@code true}, a {@code null} path location will be populated + * with a default object value and traversed instead of resulting in a + * {@link NullValueInNestedPathException}. + *

Default is {@code false} on a plain instance. + */ + void setAutoGrowNestedPaths(boolean autoGrowNestedPaths); + + /** + * Return whether "auto-growing" of nested paths has been activated. + */ + boolean isAutoGrowNestedPaths(); + } diff --git a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java index 9e6c67703c3..3a5ea914fef 100644 --- a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java @@ -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"); * you may not use this file except in compliance with the License. @@ -16,28 +16,31 @@ package org.springframework.beans; +import java.beans.PropertyChangeEvent; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; -import java.beans.PropertyChangeEvent; -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; - /** * {@link PropertyAccessor} implementation that directly accesses instance fields. * Allows for direct binding to fields instead of going through JavaBean setters. * - *

This implementation just supports fields in the actual target object. - * It is not able to traverse nested fields. + *

Since 4.1 this implementation supports nested fields traversing. * *

A DirectFieldAccessor's default for the "extractOldValueForEditor" setting * is "true", since a field can always be read without side effects. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.0 * @see #setExtractOldValueForEditor * @see BeanWrapper @@ -46,113 +49,269 @@ import java.util.Map; */ public class DirectFieldAccessor extends AbstractPropertyAccessor { - private final Object target; + private final Object rootObject; - private final Map fieldMap = new HashMap(); + private final Map fieldMap = new HashMap(); /** - * Create a new DirectFieldAccessor for the given target object. - * @param target the target object to access + * Create a new DirectFieldAccessor for the given root object. + * @param rootObject the root object to access */ - public DirectFieldAccessor(final Object target) { - Assert.notNull(target, "Target object must not be null"); - this.target = target; - ReflectionUtils.doWithFields(this.target.getClass(), new ReflectionUtils.FieldCallback() { - @Override - public void doWith(Field field) { - if (fieldMap.containsKey(field.getName())) { - // ignore superclass declarations of fields already found in a subclass - } - else { - fieldMap.put(field.getName(), field); - } - } - }); - this.typeConverterDelegate = new TypeConverterDelegate(this, target); + public DirectFieldAccessor(final Object rootObject) { + Assert.notNull(rootObject, "Root object must not be null"); + this.rootObject = rootObject; + this.typeConverterDelegate = new TypeConverterDelegate(this, rootObject); registerDefaultEditors(); setExtractOldValueForEditor(true); } + /** + * Return the root object at the top of the path of this instance. + */ + public final Object getRootInstance() { + return this.rootObject; + } + + /** + * Return the class of the root object at the top of the path of this instance. + */ + public final Class getRootClass() { + return (this.rootObject != null ? this.rootObject.getClass() : null); + } @Override public boolean isReadableProperty(String propertyName) throws BeansException { - return this.fieldMap.containsKey(propertyName); + return hasProperty(propertyName); } @Override public boolean isWritableProperty(String propertyName) throws BeansException { - return this.fieldMap.containsKey(propertyName); + return hasProperty(propertyName); } @Override - public Class getPropertyType(String propertyName) throws BeansException { - Field field = this.fieldMap.get(propertyName); - if (field != null) { - return field.getType(); + public Class getPropertyType(String propertyPath) throws BeansException { + FieldAccessor fieldAccessor = getFieldAccessor(propertyPath); + if (fieldAccessor != null) { + return fieldAccessor.getField().getType(); } return null; } @Override public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { - Field field = this.fieldMap.get(propertyName); - if (field != null) { - return new TypeDescriptor(field); + FieldAccessor fieldAccessor = getFieldAccessor(propertyName); + if (fieldAccessor != null) { + return new TypeDescriptor(fieldAccessor.getField()); } return null; } @Override public Object getPropertyValue(String propertyName) throws BeansException { - Field field = this.fieldMap.get(propertyName); - if (field == null) { + FieldAccessor fieldAccessor = getFieldAccessor(propertyName); + if (fieldAccessor == null) { throw new NotReadablePropertyException( - this.target.getClass(), propertyName, "Field '" + propertyName + "' does not exist"); - } - try { - ReflectionUtils.makeAccessible(field); - return field.get(this.target); - } - catch (IllegalAccessException ex) { - throw new InvalidPropertyException(this.target.getClass(), propertyName, "Field is not accessible", ex); + getRootClass(), propertyName, "Field '" + propertyName + "' does not exist"); } + return fieldAccessor.getValue(); } @Override public void setPropertyValue(String propertyName, Object newValue) throws BeansException { - Field field = this.fieldMap.get(propertyName); - if (field == null) { + FieldAccessor fieldAccessor = getFieldAccessor(propertyName); + if (fieldAccessor == null) { throw new NotWritablePropertyException( - this.target.getClass(), propertyName, "Field '" + propertyName + "' does not exist"); + getRootClass(), propertyName, "Field '" + propertyName + "' does not exist"); } + Field field = fieldAccessor.getField(); Object oldValue = null; try { - ReflectionUtils.makeAccessible(field); - oldValue = field.get(this.target); + oldValue = fieldAccessor.getValue(); Object convertedValue = this.typeConverterDelegate.convertIfNecessary( field.getName(), oldValue, newValue, field.getType(), new TypeDescriptor(field)); - field.set(this.target, convertedValue); + fieldAccessor.setValue(convertedValue); } catch (ConverterNotFoundException ex) { - PropertyChangeEvent pce = new PropertyChangeEvent(this.target, propertyName, oldValue, newValue); + PropertyChangeEvent pce = new PropertyChangeEvent(getRootInstance(), propertyName, oldValue, newValue); throw new ConversionNotSupportedException(pce, field.getType(), ex); } catch (ConversionException ex) { - PropertyChangeEvent pce = new PropertyChangeEvent(this.target, propertyName, oldValue, newValue); + PropertyChangeEvent pce = new PropertyChangeEvent(getRootInstance(), propertyName, oldValue, newValue); throw new TypeMismatchException(pce, field.getType(), ex); } catch (IllegalStateException ex) { - PropertyChangeEvent pce = new PropertyChangeEvent(this.target, propertyName, oldValue, newValue); + PropertyChangeEvent pce = new PropertyChangeEvent(getRootInstance(), propertyName, oldValue, newValue); throw new ConversionNotSupportedException(pce, field.getType(), ex); } catch (IllegalArgumentException ex) { - PropertyChangeEvent pce = new PropertyChangeEvent(this.target, propertyName, oldValue, newValue); + PropertyChangeEvent pce = new PropertyChangeEvent(getRootInstance(), propertyName, oldValue, newValue); throw new TypeMismatchException(pce, field.getType(), ex); } - catch (IllegalAccessException ex) { - throw new InvalidPropertyException(this.target.getClass(), propertyName, "Field is not accessible", ex); + } + + private boolean hasProperty(String propertyPath) { + Assert.notNull(propertyPath, "PropertyPath must not be null"); + return getFieldAccessor(propertyPath) != null; + } + + private FieldAccessor getFieldAccessor(String propertyPath) { + FieldAccessor fieldAccessor = this.fieldMap.get(propertyPath); + if (fieldAccessor == null) { + fieldAccessor = doGetFieldAccessor(propertyPath, getRootClass()); + this.fieldMap.put(propertyPath, fieldAccessor); + } + return fieldAccessor; + } + + private FieldAccessor doGetFieldAccessor(String propertyPath, Class targetClass) { + StringTokenizer st = new StringTokenizer(propertyPath, "."); + FieldAccessor accessor = null; + Class parentType = targetClass; + while (st.hasMoreTokens()) { + String localProperty = st.nextToken(); + Field field = ReflectionUtils.findField(parentType, localProperty); + if (field == null) { + return null; + } + if (accessor == null) { + accessor = root(propertyPath, localProperty, field); + } + else { + accessor = accessor.child(localProperty, field); + } + parentType = field.getType(); + } + return accessor; + } + + /** + * Create a root {@link FieldAccessor}. + * + * @param canonicalName the full expression for the field to access + * @param actualName the name of the local (root) property + * @param field the field accessing the property + */ + private FieldAccessor root(String canonicalName, String actualName, Field field) { + return new FieldAccessor(null, canonicalName, actualName, field); + } + + + /** + * Provide an easy access to a potentially hierarchical value. + */ + private class FieldAccessor { + + private final List parents; + + private final String canonicalName; + + private final String actualName; + + private final Field field; + + /** + * Create a new instance. + * @param parent the parent accessor, if any + * @param canonicalName the full expression for the field to access + * @param actualName the name of the partial expression for this property + * @param field the field accessing the property + */ + private FieldAccessor(FieldAccessor parent, String canonicalName, String actualName, Field field) { + Assert.notNull(canonicalName, "Expression must no be null"); + Assert.notNull(field, "Field must no be null"); + this.parents = buildParents(parent); + this.canonicalName = canonicalName; + this.actualName = actualName; + this.field = field; + } + + /** + * Create a child instance. + * + * @param actualName the name of the child property + * @param field the field accessing the child property + */ + public FieldAccessor child(String actualName, Field field) { + return new FieldAccessor(this, this.canonicalName, this.actualName + "." + actualName, field); + } + + public Field getField() { + return field; + } + + public Object getValue() { + Object localTarget = getLocalTarget(getRootInstance()); + return getParentValue(localTarget); + + } + + public void setValue(Object value) { + Object localTarget = getLocalTarget(getRootInstance()); + try { + this.field.set(localTarget, value); + } + catch (IllegalAccessException e) { + throw new InvalidPropertyException(localTarget.getClass(), canonicalName, + "Field is not accessible", e); + } } + + private Object getParentValue(Object target) { + try { + ReflectionUtils.makeAccessible(this.field); + return this.field.get(target); + } + catch (IllegalAccessException ex) { + throw new InvalidPropertyException(target.getClass(), + this.canonicalName, "Field is not accessible", ex); + } + } + + private Object getLocalTarget(Object rootTarget) { + Object localTarget = rootTarget; + for (FieldAccessor parent : parents) { + localTarget = autoGrowIfNecessary(parent, parent.getParentValue(localTarget)); + if (localTarget == null) { // Could not traverse the graph any further + throw new NullValueInNestedPathException(getRootClass(), parent.actualName, + "Cannot access indexed value of property referenced in indexed " + + "property path '" + getField().getName() + "': returned null"); + } + } + return localTarget; + } + + private Object newValue() { + Class type = getField().getType(); + try { + return type.newInstance(); + } + catch (Exception e) { + throw new NullValueInNestedPathException(getRootClass(), this.actualName, + "Could not instantiate property type [" + type.getName() + "] to " + + "auto-grow nested property path: " + e); + } + } + + private Object autoGrowIfNecessary(FieldAccessor accessor, Object value) { + if (value == null && isAutoGrowNestedPaths()) { + Object defaultValue = accessor.newValue(); + accessor.setValue(defaultValue); + return defaultValue; + } + return value; + } + + private List buildParents(FieldAccessor parent) { + List parents = new ArrayList(); + if (parent != null) { + parents.addAll(parent.parents); + parents.add(parent); + } + return parents; + } + } } diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractConfigurablePropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractConfigurablePropertyAccessorTests.java new file mode 100644 index 00000000000..df70845ee84 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractConfigurablePropertyAccessorTests.java @@ -0,0 +1,400 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * + * @author Stephane Nicoll + */ +public abstract class AbstractConfigurablePropertyAccessorTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + + protected abstract ConfigurablePropertyAccessor createAccessor(Object target); + + @Test + public void isReadableProperty() { + ConfigurablePropertyAccessor accessor = createAccessor(new Simple("John", 2)); + + assertThat(accessor.isReadableProperty("name"), is(true)); + } + + @Test + public void isReadablePropertyNotReadable() { + ConfigurablePropertyAccessor accessor = createAccessor(new NoRead()); + + assertFalse(accessor.isReadableProperty("age")); + } + + /** + * Shouldn't throw an exception: should just return false + */ + @Test + public void isReadablePropertyNoSuchProperty() { + ConfigurablePropertyAccessor accessor = createAccessor(new NoRead()); + + assertFalse(accessor.isReadableProperty("xxxxx")); + } + + @Test + public void isReadablePropertyNull() { + ConfigurablePropertyAccessor accessor = createAccessor(new NoRead()); + + thrown.expect(IllegalArgumentException.class); + accessor.isReadableProperty(null); + } + + @Test + public void isWritableProperty() { + ConfigurablePropertyAccessor accessor = createAccessor(new Simple("John", 2)); + + assertThat(accessor.isWritableProperty("name"), is(true)); + } + + @Test + public void isWritablePropertyNull() { + ConfigurablePropertyAccessor accessor = createAccessor(new NoRead()); + + thrown.expect(IllegalArgumentException.class); + accessor.isWritableProperty(null); + } + + @Test + public void isWritablePropertyNoSuchProperty() { + ConfigurablePropertyAccessor accessor = createAccessor(new NoRead()); + + assertFalse(accessor.isWritableProperty("xxxxx")); + } + + @Test + public void getSimpleProperty() { + Simple simple = new Simple("John", 2); + ConfigurablePropertyAccessor accessor = createAccessor(simple); + assertThat(accessor.getPropertyValue("name"), is("John")); + } + + @Test + public void getNestedProperty() { + Person person = createPerson("John", "London", "UK"); + ConfigurablePropertyAccessor accessor = createAccessor(person); + assertThat(accessor.getPropertyValue("address.city"), is("London")); + } + + @Test + public void getNestedDeepProperty() { + Person person = createPerson("John", "London", "UK"); + ConfigurablePropertyAccessor accessor = createAccessor(person); + + assertThat(accessor.getPropertyValue("address.country.name"), is("UK")); + } + + @Test + public void getPropertyIntermediateFieldIsNull() { + Person person = createPerson("John", "London", "UK"); + person.address = null; + ConfigurablePropertyAccessor accessor = createAccessor(person); + + try { + accessor.getPropertyValue("address.country.name"); + fail("Should have failed to get value with null intermediate path"); + } + catch (NullValueInNestedPathException e) { + assertEquals("address", e.getPropertyName()); + assertEquals(Person.class, e.getBeanClass()); + } + } + + @Test + public void getPropertyIntermediateFieldIsNullWithAutoGrow() { + Person person = createPerson("John", "London", "UK"); + person.address = null; + ConfigurablePropertyAccessor accessor = createAccessor(person); + accessor.setAutoGrowNestedPaths(true); + + assertEquals("DefaultCountry", accessor.getPropertyValue("address.country.name")); + } + + @Test + public void getUnknownField() { + Simple simple = new Simple("John", 2); + ConfigurablePropertyAccessor accessor = createAccessor(simple); + + try { + accessor.getPropertyValue("foo"); + fail("Should have failed to get an unknown field."); + } + catch (NotReadablePropertyException e) { + assertEquals(Simple.class, e.getBeanClass()); + assertEquals("foo", e.getPropertyName()); + } + } + + @Test + public void getUnknownNestedField() { + Person person = createPerson("John", "London", "UK"); + ConfigurablePropertyAccessor accessor = createAccessor(person); + + thrown.expect(NotReadablePropertyException.class); + accessor.getPropertyValue("address.bar"); + } + + @Test + public void setSimpleProperty() { + Simple simple = new Simple("John", 2); + ConfigurablePropertyAccessor accessor = createAccessor(simple); + + accessor.setPropertyValue("name", "SomeValue"); + + assertThat(simple.name, is("SomeValue")); + assertThat(simple.getName(), is("SomeValue")); + } + + @Test + public void setNestedProperty() { + Person person = createPerson("John", "Paris", "FR"); + ConfigurablePropertyAccessor accessor = createAccessor(person); + + accessor.setPropertyValue("address.city", "London"); + assertThat(person.address.city, is("London")); + } + + @Test + public void setNestedDeepProperty() { + Person person = createPerson("John", "Paris", "FR"); + ConfigurablePropertyAccessor accessor = createAccessor(person); + + accessor.setPropertyValue("address.country.name", "UK"); + assertThat(person.address.country.name, is("UK")); + } + + @Test + public void setPropertyIntermediateFieldIsNull() { + Person person = createPerson("John", "Paris", "FR"); + person.address.country = null; + ConfigurablePropertyAccessor accessor = createAccessor(person); + + try { + accessor.setPropertyValue("address.country.name", "UK"); + fail("Should have failed to set value with intermediate null value"); + } + catch (NullValueInNestedPathException e) { + assertEquals("address.country", e.getPropertyName()); + assertEquals(Person.class, e.getBeanClass()); + } + assertThat(person.address.country, is(nullValue())); // Not touched + } + + @Test + public void setPropertyIntermediateFieldIsNullWithAutoGrow() { + Person person = createPerson("John", "Paris", "FR"); + person.address.country = null; + ConfigurablePropertyAccessor accessor = createAccessor(person); + accessor.setAutoGrowNestedPaths(true); + + accessor.setPropertyValue("address.country.name", "UK"); + assertThat(person.address.country.name, is("UK")); + } + + @Test + public void setUnknownField() { + Simple simple = new Simple("John", 2); + ConfigurablePropertyAccessor accessor = createAccessor(simple); + + try { + accessor.setPropertyValue("foo", "value"); + fail("Should have failed to set an unknown field."); + } + catch (NotWritablePropertyException e) { + assertEquals(Simple.class, e.getBeanClass()); + assertEquals("foo", e.getPropertyName()); + } + } + + @Test + public void setUnknownNestedField() { + Person person = createPerson("John", "Paris", "FR"); + ConfigurablePropertyAccessor accessor = createAccessor(person); + + thrown.expect(NotWritablePropertyException.class); + accessor.setPropertyValue("address.bar", "value"); + } + + + @Test + public void propertyType() { + Person person = createPerson("John", "Paris", "FR"); + ConfigurablePropertyAccessor accessor = createAccessor(person); + + assertEquals(String.class, accessor.getPropertyType("address.city")); + } + + @Test + public void propertyTypeUnknownField() { + Simple simple = new Simple("John", 2); + ConfigurablePropertyAccessor accessor = createAccessor(simple); + + assertThat(accessor.getPropertyType("foo"), is(nullValue())); + } + + @Test + public void propertyTypeDescriptor() { + Person person = createPerson("John", "Paris", "FR"); + ConfigurablePropertyAccessor accessor = createAccessor(person); + + assertThat(accessor.getPropertyTypeDescriptor("address.city"), is(notNullValue())); + } + + @Test + public void propertyTypeDescriptorUnknownField() { + Simple simple = new Simple("John", 2); + ConfigurablePropertyAccessor accessor = createAccessor(simple); + + assertThat(accessor.getPropertyTypeDescriptor("foo"), is(nullValue())); + } + + + private Person createPerson(String name, String city, String country) { + return new Person(name, new Address(city, country)); + } + + + private static class Simple { + + private String name; + + private Integer integer; + + private Simple(String name, Integer integer) { + this.name = name; + this.integer = integer; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getInteger() { + return integer; + } + + public void setInteger(Integer integer) { + this.integer = integer; + } + } + + private static class Person { + private String name; + + private Address address; + + private Person(String name, Address address) { + this.name = name; + this.address = address; + } + + public Person() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + } + + private static class Address { + private String city; + + private Country country; + + private Address(String city, String country) { + this.city = city; + this.country = new Country(country); + } + + public Address() { + this("DefaultCity", "DefaultCountry"); + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public Country getCountry() { + return country; + } + + public void setCountry(Country country) { + this.country = country; + } + } + + private static class Country { + private String name; + + public Country(String name) { + this.name = name; + } + + public Country() { + this(null); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + @SuppressWarnings("unused") + static class NoRead { + + public void setAge(int age) { + } + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java index 7533d39a7b3..705f1f70116 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java @@ -68,7 +68,12 @@ import static org.junit.Assert.*; * @author Chris Beams * @author Dave Syer */ -public final class BeanWrapperTests { +public final class BeanWrapperTests extends AbstractConfigurablePropertyAccessorTests { + + @Override + protected ConfigurablePropertyAccessor createAccessor(Object target) { + return new BeanWrapperImpl(target); + } @Test public void testNullNestedTypeDescriptor() { @@ -116,48 +121,6 @@ public final class BeanWrapperTests { assertEquals("9", foo.listOfMaps.get(0).get("luckyNumber")); } - @Test - public void testIsReadablePropertyNotReadable() { - NoRead nr = new NoRead(); - BeanWrapper bw = new BeanWrapperImpl(nr); - assertFalse(bw.isReadableProperty("age")); - } - - /** - * Shouldn't throw an exception: should just return false - */ - @Test - public void testIsReadablePropertyNoSuchProperty() { - NoRead nr = new NoRead(); - BeanWrapper bw = new BeanWrapperImpl(nr); - assertFalse(bw.isReadableProperty("xxxxx")); - } - - @Test - public void testIsReadablePropertyNull() { - NoRead nr = new NoRead(); - BeanWrapper bw = new BeanWrapperImpl(nr); - try { - bw.isReadableProperty(null); - fail("Can't inquire into readability of null property"); - } - catch (IllegalArgumentException ex) { - // expected - } - } - - @Test - public void testIsWritablePropertyNull() { - NoRead nr = new NoRead(); - BeanWrapper bw = new BeanWrapperImpl(nr); - try { - bw.isWritableProperty(null); - fail("Can't inquire into writability of null property"); - } - catch (IllegalArgumentException ex) { - // expected - } - } @Test public void testReadableAndWritableForIndexedProperties() { @@ -1636,12 +1599,6 @@ public final class BeanWrapperTests { } - @SuppressWarnings("unused") - private static class NoRead { - - public void setAge(int age) { - } - } @SuppressWarnings("unused") diff --git a/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java index 01a117d2b61..aac5667f4e4 100644 --- a/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,12 @@ import org.junit.Test; * @author Jose Luis Martin * @author Chris Beams */ -public class DirectFieldAccessorTests { +public class DirectFieldAccessorTests extends AbstractConfigurablePropertyAccessorTests { + + @Override + protected ConfigurablePropertyAccessor createAccessor(Object target) { + return new DirectFieldAccessor(target); + } @Test public void withShadowedField() throws Exception { @@ -42,4 +47,5 @@ public class DirectFieldAccessorTests { DirectFieldAccessor dfa = new DirectFieldAccessor(p); assertEquals(JTextField.class, dfa.getPropertyType("name")); } + } diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index dd4d4be385d..8d5b1af7008 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -189,8 +189,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { *

If "true", a null path location will be populated with a default object value and traversed * instead of resulting in an exception. This flag also enables auto-growth of collection elements * when accessing an out-of-bounds index. - *

Default is "true" on a standard DataBinder. Note that this feature is only supported - * for bean property access (DataBinder's default mode), not for field access. + *

Default is "true" on a standard DataBinder. Note that since Spring 4.1 this feature is supported + * for bean property access (DataBinder's default mode) and field access. * @see #initBeanPropertyAccess() * @see org.springframework.beans.BeanWrapper#setAutoGrowNestedPaths */