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
*/