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;
}
}
}