Support for nested path using field access

This commit adds a nested path support for DirectFieldAccessor that is
similar to what BeanWrapper provides. It is now possible to use
expressions such as "person.address.city.name" to access the name of
the city that a given person lives in using fields to traverse the
graph.

DirectFieldAccessor also now supports an auto-grow option to create
a default instance for a "null" intermediate path. This option is
false by default and leads to a NullValueInNestedPathException in such
a case.

This commit also harmonizes part of the tests suite so that core tests
are shared between BeanWrapperImpl and DirectFieldAccessor.

Note that map and list access is not implemented as part of this
commit.

Issue: SPR-9705
This commit is contained in:
Stephane Nicoll 2014-06-26 19:19:56 +02:00
parent 442bd682a7
commit 8221c9abc5
9 changed files with 670 additions and 155 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");
* 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 {

View File

@ -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.
* <p>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.
* <p>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.
* <p>Default is unlimited on a plain BeanWrapper.

View File

@ -115,8 +115,6 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
*/
private Map<String, BeanWrapperImpl> 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.
* <p>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.
* <p>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<Object> 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);

View File

@ -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;
* <p>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.
* <p>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}.
* <p>Default is {@code false} on a plain instance.
*/
void setAutoGrowNestedPaths(boolean autoGrowNestedPaths);
/**
* Return whether "auto-growing" of nested paths has been activated.
*/
boolean isAutoGrowNestedPaths();
}

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");
* 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.
*
* <p>This implementation just supports fields in the actual target object.
* It is not able to traverse nested fields.
* <p>Since 4.1 this implementation supports nested fields traversing.
*
* <p>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<String, Field> fieldMap = new HashMap<String, Field>();
private final Map<String, FieldAccessor> fieldMap = new HashMap<String, FieldAccessor>();
/**
* 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<FieldAccessor> 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<FieldAccessor> buildParents(FieldAccessor parent) {
List<FieldAccessor> parents = new ArrayList<FieldAccessor>();
if (parent != null) {
parents.addAll(parent.parents);
parents.add(parent);
}
return parents;
}
}
}

View File

@ -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) {
}
}
}

View File

@ -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")

View File

@ -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"));
}
}

View File

@ -189,8 +189,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* <p>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.
* <p>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.
* <p>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
*/