From 2dc674f356fbe941b98301f89494767f80d55e6a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 21 May 2015 19:58:57 +0200 Subject: [PATCH] Restore AbstractPropertyAccessor Commit 3d86f15 added a lot of new feature in AbstractPropertyAccessor shared by both DirectFieldAccessor and BeanWrapperImpl. This makes this abstract class harder to implement for others having simpler use cases. It turns that Spring Data has such use case; this commit split these new features in a dedicated new base class, leaving AbstractPropertyAccessor untouched. Issue: SPR-12805 --- .../AbstractNestablePropertyAccessor.java | 1058 +++++++++++++++++ .../beans/AbstractPropertyAccessor.java | 1040 +--------------- .../beans/BeanWrapperImpl.java | 2 +- .../beans/DirectFieldAccessor.java | 2 +- ...ava => AbstractPropertyAccessorTests.java} | 4 +- .../beans/BeanWrapperTests.java | 2 +- .../beans/DirectFieldAccessorTests.java | 2 +- 7 files changed, 1081 insertions(+), 1029 deletions(-) create mode 100644 spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java rename spring-beans/src/test/java/org/springframework/beans/{AbstractConfigurablePropertyAccessorTests.java => AbstractPropertyAccessorTests.java} (99%) diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java new file mode 100644 index 0000000000..ba9e309ace --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -0,0 +1,1058 @@ +/* + * Copyright 2002-2015 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 java.beans.PropertyChangeEvent; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.PrivilegedActionException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.UsesJava8; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * A basic {@link ConfigurablePropertyAccessor} that provides the necessary + * infrastructure for all typical use cases. + * + *

This accessor will convert collection and array values to the corresponding + * target collections or arrays, if necessary. Custom property editors that deal + * with collections or arrays can either be written via PropertyEditor's + * {@code setValue}, or against a comma-delimited String via {@code setAsText}, + * as String arrays are converted in such a format if the array itself is not + * assignable. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Stephane Nicoll + * @since 4.2 + * @see #registerCustomEditor + * @see #setPropertyValues + * @see #setPropertyValue + * @see #getPropertyValue + * @see #getPropertyType + * @see BeanWrapper + * @see PropertyEditorRegistrySupport + */ +public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyAccessor { + + /** + * We'll create a lot of these objects, so we don't want a new logger every time. + */ + private static final Log logger = LogFactory.getLog(AbstractNestablePropertyAccessor.class); + + private static Class javaUtilOptionalClass = null; + + static { + try { + javaUtilOptionalClass = + ClassUtils.forName("java.util.Optional", AbstractNestablePropertyAccessor.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Java 8 not available - Optional references simply not supported then. + } + } + + private int autoGrowCollectionLimit = Integer.MAX_VALUE; + + /** The wrapped object */ + private Object object; + + private String nestedPath = ""; + + private Object rootObject; + + /** + * Map with cached nested Accessors: nested path -> Accessor instance. + */ + private Map nestedPropertyAccessors; + + /** + * Create new empty accessor. Wrapped instance needs to be set afterwards. + * Registers default editors. + * @see #setWrappedInstance + */ + protected AbstractNestablePropertyAccessor() { + this(true); + } + + /** + * Create new empty accessor. Wrapped instance needs to be set afterwards. + * @param registerDefaultEditors whether to register default editors + * (can be suppressed if the accessor won't need any type conversion) + * @see #setWrappedInstance + */ + protected AbstractNestablePropertyAccessor(boolean registerDefaultEditors) { + if (registerDefaultEditors) { + registerDefaultEditors(); + } + this.typeConverterDelegate = new TypeConverterDelegate(this); + } + + /** + * Create new accessor for the given object. + * @param object object wrapped by this accessor + */ + protected AbstractNestablePropertyAccessor(Object object) { + registerDefaultEditors(); + setWrappedInstance(object); + } + + /** + * Create new accessor, wrapping a new instance of the specified class. + * @param clazz class to instantiate and wrap + */ + protected AbstractNestablePropertyAccessor(Class clazz) { + registerDefaultEditors(); + setWrappedInstance(BeanUtils.instantiateClass(clazz)); + } + + /** + * Create new accessor for the given object, + * registering a nested path that the object is in. + * @param object object wrapped by this accessor + * @param nestedPath the nested path of the object + * @param rootObject the root object at the top of the path + */ + protected AbstractNestablePropertyAccessor(Object object, String nestedPath, Object rootObject) { + registerDefaultEditors(); + setWrappedInstance(object, nestedPath, rootObject); + } + + /** + * Create new accessor for the given object, + * registering a nested path that the object is in. + * @param object object wrapped by this accessor + * @param nestedPath the nested path of the object + * @param parent the containing accessor (must not be {@code null}) + */ + protected AbstractNestablePropertyAccessor(Object object, String nestedPath, AbstractNestablePropertyAccessor parent) { + setWrappedInstance(object, nestedPath, parent.getWrappedInstance()); + setExtractOldValueForEditor(parent.isExtractOldValueForEditor()); + setAutoGrowNestedPaths(parent.isAutoGrowNestedPaths()); + setAutoGrowCollectionLimit(parent.getAutoGrowCollectionLimit()); + setConversionService(parent.getConversionService()); + } + + /** + * Specify a limit for array and collection auto-growing. + *

Default is unlimited on a plain accessor. + */ + public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { + this.autoGrowCollectionLimit = autoGrowCollectionLimit; + } + + /** + * Return the limit for array and collection auto-growing. + */ + public int getAutoGrowCollectionLimit() { + return this.autoGrowCollectionLimit; + } + + /** + * Switch the target object, replacing the cached introspection results only + * if the class of the new object is different to that of the replaced object. + * @param object the new target object + */ + public void setWrappedInstance(Object object) { + setWrappedInstance(object, "", null); + } + + /** + * Switch the target object, replacing the cached introspection results only + * if the class of the new object is different to that of the replaced object. + * @param object the new target object + * @param nestedPath the nested path of the object + * @param rootObject the root object at the top of the path + */ + public void setWrappedInstance(Object object, String nestedPath, Object rootObject) { + Assert.notNull(object, "Bean object must not be null"); + if (object.getClass().equals(javaUtilOptionalClass)) { + this.object = OptionalUnwrapper.unwrap(object); + } + else { + this.object = object; + } + this.nestedPath = (nestedPath != null ? nestedPath : ""); + this.rootObject = (!"".equals(this.nestedPath) ? rootObject : this.object); + this.nestedPropertyAccessors = null; + this.typeConverterDelegate = new TypeConverterDelegate(this, this.object); + } + + public final Object getWrappedInstance() { + return this.object; + } + + public final Class getWrappedClass() { + return (this.object != null ? this.object.getClass() : null); + } + + /** + * Return the nested path of the object wrapped by this accessor. + */ + public final String getNestedPath() { + return this.nestedPath; + } + + /** + * Return the root object at the top of the path of this accessor. + * @see #getNestedPath + */ + public final Object getRootInstance() { + return this.rootObject; + } + + /** + * Return the class of the root object at the top of the path of this accessor. + * @see #getNestedPath + */ + public final Class getRootClass() { + return (this.rootObject != null ? this.rootObject.getClass() : null); + } + + @Override + public void setPropertyValue(String propertyName, Object value) throws BeansException { + AbstractNestablePropertyAccessor nestedPa; + try { + nestedPa = getPropertyAccessorForPropertyPath(propertyName); + } + catch (NotReadablePropertyException ex) { + throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, + "Nested property in path '" + propertyName + "' does not exist", ex); + } + PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); + nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value)); + } + + @Override + public void setPropertyValue(PropertyValue pv) throws BeansException { + PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens; + if (tokens == null) { + String propertyName = pv.getName(); + AbstractNestablePropertyAccessor nestedPa; + try { + nestedPa = getPropertyAccessorForPropertyPath(propertyName); + } + catch (NotReadablePropertyException ex) { + throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, + "Nested property in path '" + propertyName + "' does not exist", ex); + } + tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); + if (nestedPa == this) { + pv.getOriginalPropertyValue().resolvedTokens = tokens; + } + nestedPa.setPropertyValue(tokens, pv); + } + else { + setPropertyValue(tokens, pv); + } + } + + @SuppressWarnings("unchecked") + protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { + String propertyName = tokens.canonicalName; + String actualName = tokens.actualName; + + if (tokens.keys != null) { + // Apply indexes and map keys: fetch value for all keys but the last one. + PropertyTokenHolder getterTokens = new PropertyTokenHolder(); + getterTokens.canonicalName = tokens.canonicalName; + getterTokens.actualName = tokens.actualName; + getterTokens.keys = new String[tokens.keys.length - 1]; + System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1); + Object propValue; + try { + propValue = getPropertyValue(getterTokens); + } + catch (NotReadablePropertyException ex) { + throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, + "Cannot access indexed value in property referenced " + + "in indexed property path '" + propertyName + "'", ex); + } + // Set value for last key. + String key = tokens.keys[tokens.keys.length - 1]; + if (propValue == null) { + // null map value case + if (isAutoGrowNestedPaths()) { + // TODO: cleanup, this is pretty hacky + int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); + getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex); + propValue = setDefaultValue(getterTokens); + } + else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, + "Cannot access indexed value in property referenced " + + "in indexed property path '" + propertyName + "': returned null"); + } + } + if (propValue.getClass().isArray()) { + PropertyHandler ph = getLocalPropertyHandler(actualName); + Class requiredType = propValue.getClass().getComponentType(); + int arrayIndex = Integer.parseInt(key); + Object oldValue = null; + try { + if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) { + oldValue = Array.get(propValue, arrayIndex); + } + Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(), + requiredType, ph.nested(tokens.keys.length)); + int length = Array.getLength(propValue); + if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { + Class componentType = propValue.getClass().getComponentType(); + Object newArray = Array.newInstance(componentType, arrayIndex + 1); + System.arraycopy(propValue, 0, newArray, 0, length); + setPropertyValue(actualName, newArray); + propValue = getPropertyValue(actualName); + } + Array.set(propValue, arrayIndex, convertedValue); + } + catch (IndexOutOfBoundsException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Invalid array index in property path '" + propertyName + "'", ex); + } + } + else if (propValue instanceof List) { + PropertyHandler ph = getPropertyHandler(actualName); + Class requiredType = ph.getCollectionType(tokens.keys.length); + List list = (List) propValue; + int index = Integer.parseInt(key); + Object oldValue = null; + if (isExtractOldValueForEditor() && index < list.size()) { + oldValue = list.get(index); + } + Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(), + requiredType, ph.nested(tokens.keys.length)); + int size = list.size(); + if (index >= size && index < this.autoGrowCollectionLimit) { + for (int i = size; i < index; i++) { + try { + list.add(null); + } + catch (NullPointerException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Cannot set element with index " + index + " in List of size " + + size + ", accessed using property path '" + propertyName + + "': List does not support filling up gaps with null elements"); + } + } + list.add(convertedValue); + } + else { + try { + list.set(index, convertedValue); + } + catch (IndexOutOfBoundsException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Invalid list index in property path '" + propertyName + "'", ex); + } + } + } + else if (propValue instanceof Map) { + PropertyHandler ph = getLocalPropertyHandler(actualName); + Class mapKeyType = ph.getMapKeyType(tokens.keys.length); + Class mapValueType = ph.getMapValueType(tokens.keys.length); + Map map = (Map) propValue; + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); + Object oldValue = null; + if (isExtractOldValueForEditor()) { + oldValue = map.get(convertedMapKey); + } + // Pass full property name and old value in here, since we want full + // conversion ability for map values. + Object convertedMapValue = convertIfNecessary(propertyName, oldValue, pv.getValue(), + mapValueType, ph.nested(tokens.keys.length)); + map.put(convertedMapKey, convertedMapValue); + } + else { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Property referenced in indexed property path '" + propertyName + + "' is neither an array nor a List nor a Map; returned value was [" + propValue + "]"); + } + } + + else { + PropertyHandler ph = getLocalPropertyHandler(actualName); + if (ph == null || !ph.isWritable()) { + if (pv.isOptional()) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring optional value for property '" + actualName + + "' - property not found on bean class [" + getRootClass().getName() + "]"); + } + return; + } + else { + throw createNotWritablePropertyException(propertyName); + } + } + Object oldValue = null; + try { + Object originalValue = pv.getValue(); + Object valueToApply = originalValue; + if (!Boolean.FALSE.equals(pv.conversionNecessary)) { + if (pv.isConverted()) { + valueToApply = pv.getConvertedValue(); + } + else { + if (isExtractOldValueForEditor() && ph.isReadable()) { + try { + oldValue = ph.getValue(); + } + catch (Exception ex) { + if (ex instanceof PrivilegedActionException) { + ex = ((PrivilegedActionException) ex).getException(); + } + if (logger.isDebugEnabled()) { + logger.debug("Could not read previous value of property '" + + this.nestedPath + propertyName + "'", ex); + } + } + } + valueToApply = convertForProperty( + propertyName, oldValue, originalValue, ph.toTypeDescriptor()); + } + pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue); + } + ph.setValue(object, valueToApply); + } + catch (TypeMismatchException ex) { + throw ex; + } + catch (InvocationTargetException ex) { + PropertyChangeEvent propertyChangeEvent = + new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); + if (ex.getTargetException() instanceof ClassCastException) { + throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), ex.getTargetException()); + } + else { + Throwable cause = ex.getTargetException(); + if (cause instanceof UndeclaredThrowableException) { + // May happen e.g. with Groovy-generated methods + cause = cause.getCause(); + } + throw new MethodInvocationException(propertyChangeEvent, cause); + } + } + catch (Exception ex) { + PropertyChangeEvent pce = + new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); + throw new MethodInvocationException(pce, ex); + } + } + } + + @Override + public Class getPropertyType(String propertyName) throws BeansException { + try { + PropertyHandler ph = getPropertyHandler(propertyName); + if (ph != null) { + return ph.getPropertyType(); + } + else { + // Maybe an indexed/mapped property... + Object value = getPropertyValue(propertyName); + if (value != null) { + return value.getClass(); + } + // Check to see if there is a custom editor, + // which might give an indication on the desired target type. + Class editorType = guessPropertyTypeFromEditors(propertyName); + if (editorType != null) { + return editorType; + } + } + } + catch (InvalidPropertyException ex) { + // Consider as not determinable. + } + return null; + } + + @Override + public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { + try { + AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); + String finalPath = getFinalPath(nestedPa, propertyName); + PropertyTokenHolder tokens = getPropertyNameTokens(finalPath); + PropertyHandler ph = nestedPa.getLocalPropertyHandler(tokens.actualName); + if (ph != null) { + if (tokens.keys != null) { + if (ph.isReadable() || ph.isWritable()) { + return ph.nested(tokens.keys.length); + } + } + else { + if (ph.isReadable() || ph.isWritable()) { + return ph.toTypeDescriptor(); + } + } + } + } + catch (InvalidPropertyException ex) { + // Consider as not determinable. + } + return null; + } + + @Override + public boolean isReadableProperty(String propertyName) { + try { + PropertyHandler ph = getPropertyHandler(propertyName); + if (ph != null) { + return ph.isReadable(); + } + else { + // Maybe an indexed/mapped property... + getPropertyValue(propertyName); + return true; + } + } + catch (InvalidPropertyException ex) { + // Cannot be evaluated, so can't be readable. + } + return false; + } + + @Override + public boolean isWritableProperty(String propertyName) { + try { + PropertyHandler ph = getPropertyHandler(propertyName); + if (ph != null) { + return ph.isWritable(); + } + else { + // Maybe an indexed/mapped property... + getPropertyValue(propertyName); + return true; + } + } + catch (InvalidPropertyException ex) { + // Cannot be evaluated, so can't be writable. + } + return false; + } + + private Object convertIfNecessary(String propertyName, Object oldValue, Object newValue, Class requiredType, + TypeDescriptor td) throws TypeMismatchException { + try { + return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td); + } + catch (ConverterNotFoundException ex) { + PropertyChangeEvent pce = + new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); + throw new ConversionNotSupportedException(pce, td.getType(), ex); + } + catch (ConversionException ex) { + PropertyChangeEvent pce = + new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); + throw new TypeMismatchException(pce, requiredType, ex); + } + catch (IllegalStateException ex) { + PropertyChangeEvent pce = + new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); + throw new ConversionNotSupportedException(pce, requiredType, ex); + } + catch (IllegalArgumentException ex) { + PropertyChangeEvent pce = + new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); + throw new TypeMismatchException(pce, requiredType, ex); + } + } + + protected Object convertForProperty(String propertyName, Object oldValue, Object newValue, TypeDescriptor td) + throws TypeMismatchException { + + return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td); + } + + @Override + public Object getPropertyValue(String propertyName) throws BeansException { + AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); + PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); + return nestedPa.getPropertyValue(tokens); + } + + @SuppressWarnings("unchecked") + protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { + String propertyName = tokens.canonicalName; + String actualName = tokens.actualName; + PropertyHandler ph = getLocalPropertyHandler(actualName); + if (ph == null || !ph.isReadable()) { + throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName); + } + try { + Object value = ph.getValue(); + if (tokens.keys != null) { + if (value == null) { + if (isAutoGrowNestedPaths()) { + value = setDefaultValue(tokens.actualName); + } + else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, + "Cannot access indexed value of property referenced in indexed " + + "property path '" + propertyName + "': returned null"); + } + } + String indexedPropertyName = tokens.actualName; + // apply indexes and map keys + for (int i = 0; i < tokens.keys.length; i++) { + String key = tokens.keys[i]; + if (value == null) { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, + "Cannot access indexed value of property referenced in indexed " + + "property path '" + propertyName + "': returned null"); + } + else if (value.getClass().isArray()) { + int index = Integer.parseInt(key); + value = growArrayIfNecessary(value, index, indexedPropertyName); + value = Array.get(value, index); + } + else if (value instanceof List) { + int index = Integer.parseInt(key); + List list = (List) value; + growCollectionIfNecessary(list, index, indexedPropertyName, ph, i + 1); + value = list.get(index); + } + else if (value instanceof Set) { + // Apply index to Iterator in case of a Set. + Set set = (Set) value; + int index = Integer.parseInt(key); + if (index < 0 || index >= set.size()) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Cannot get element with index " + index + " from Set of size " + + set.size() + ", accessed using property path '" + propertyName + "'"); + } + Iterator it = set.iterator(); + for (int j = 0; it.hasNext(); j++) { + Object elem = it.next(); + if (j == index) { + value = elem; + break; + } + } + } + else if (value instanceof Map) { + Map map = (Map) value; + Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); + value = map.get(convertedMapKey); + } + else { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Property referenced in indexed property path '" + propertyName + + "' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]"); + } + indexedPropertyName += PROPERTY_KEY_PREFIX + key + PROPERTY_KEY_SUFFIX; + } + } + return value; + } + catch (IndexOutOfBoundsException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Index of out of bounds in property path '" + propertyName + "'", ex); + } + catch (NumberFormatException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Invalid index in property path '" + propertyName + "'", ex); + } + catch (TypeMismatchException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Invalid index in property path '" + propertyName + "'", ex); + } + catch (InvocationTargetException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Getter for property '" + actualName + "' threw exception", ex); + } + catch (Exception ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Illegal attempt to get property '" + actualName + "' threw exception", ex); + } + } + + + /** + * Return the {@link PropertyHandler} for the specified {@code propertyName}, navigating + * if necessary. Return {@code null} if not found rather than throwing an exception. + * @param propertyName the property to obtain the descriptor for + * @return the property descriptor for the specified property, + * or {@code null} if not found + * @throws BeansException in case of introspection failure + */ + protected PropertyHandler getPropertyHandler(String propertyName) throws BeansException { + Assert.notNull(propertyName, "Property name must not be null"); + AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); + return nestedPa.getLocalPropertyHandler(getFinalPath(nestedPa, propertyName)); + } + + /** + * Return a {@link PropertyHandler} for the specified local {@code propertyName}. Only + * used to reach a property available in the current context. + * @param propertyName the name of a local property + * @return the handler for that property or {@code null} if it has not been found + */ + protected abstract PropertyHandler getLocalPropertyHandler(String propertyName); + + /** + * Create a new nested property accessor instance. + * Can be overridden in subclasses to create a PropertyAccessor subclass. + * @param object object wrapped by this PropertyAccessor + * @param nestedPath the nested path of the object + * @return the nested PropertyAccessor instance + */ + protected abstract AbstractNestablePropertyAccessor newNestedPropertyAccessor(Object object, String nestedPath); + + /** + * Create a {@link NotWritablePropertyException} for the specified property. + */ + protected abstract NotWritablePropertyException createNotWritablePropertyException(String propertyName); + + + private Object growArrayIfNecessary(Object array, int index, String name) { + if (!isAutoGrowNestedPaths()) { + return array; + } + int length = Array.getLength(array); + if (index >= length && index < this.autoGrowCollectionLimit) { + Class componentType = array.getClass().getComponentType(); + Object newArray = Array.newInstance(componentType, index + 1); + System.arraycopy(array, 0, newArray, 0, length); + for (int i = length; i < Array.getLength(newArray); i++) { + Array.set(newArray, i, newValue(componentType, null, name)); + } + // TODO this is not efficient because conversion may create a copy ... set directly because we know it is assignable. + setPropertyValue(name, newArray); + return getPropertyValue(name); + } + else { + return array; + } + } + + private void growCollectionIfNecessary(Collection collection, int index, String name, + PropertyHandler ph, int nestingLevel) { + + if (!isAutoGrowNestedPaths()) { + return; + } + int size = collection.size(); + if (index >= size && index < this.autoGrowCollectionLimit) { + Class elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); + if (elementType != null) { + for (int i = collection.size(); i < index + 1; i++) { + collection.add(newValue(elementType, null, name)); + } + } + } + } + + /** + * Get the last component of the path. Also works if not nested. + * @param pa property accessor to work on + * @param nestedPath property path we know is nested + * @return last component of the path (the property on the target bean) + */ + private String getFinalPath(AbstractNestablePropertyAccessor pa, String nestedPath) { + if (pa == this) { + return nestedPath; + } + return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1); + } + + /** + * Recursively navigate to return a property accessor for the nested property path. + * @param propertyPath property property path, which may be nested + * @return a property accessor for the target bean + */ + @SuppressWarnings("unchecked") // avoid nested generic + protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) { + int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath); + // Handle nested properties recursively. + if (pos > -1) { + String nestedProperty = propertyPath.substring(0, pos); + String nestedPath = propertyPath.substring(pos + 1); + AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty); + return nestedPa.getPropertyAccessorForPropertyPath(nestedPath); + } + else { + return this; + } + } + + /** + * Retrieve a Property accessor for the given nested property. + * Create a new one if not found in the cache. + *

Note: Caching nested PropertyAccessors is necessary now, + * to keep registered custom editors for nested properties. + * @param nestedProperty property to create the PropertyAccessor for + * @return the PropertyAccessor instance, either cached or newly created + */ + private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) { + if (this.nestedPropertyAccessors == null) { + this.nestedPropertyAccessors = new HashMap(); + } + // Get value of bean property. + PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); + String canonicalName = tokens.canonicalName; + Object value = getPropertyValue(tokens); + if (value == null || (value.getClass().equals(javaUtilOptionalClass) && OptionalUnwrapper.isEmpty(value))) { + if (isAutoGrowNestedPaths()) { + value = setDefaultValue(tokens); + } + else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName); + } + } + + // Lookup cached sub-PropertyAccessor, create new one if not found. + AbstractNestablePropertyAccessor nestedPa = this.nestedPropertyAccessors.get(canonicalName); + if (nestedPa == null || nestedPa.getWrappedInstance() != + (value.getClass().equals(javaUtilOptionalClass) ? OptionalUnwrapper.unwrap(value) : value)) { + if (logger.isTraceEnabled()) { + logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'"); + } + nestedPa = newNestedPropertyAccessor(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR); + // Inherit all type-specific PropertyEditors. + copyDefaultEditorsTo(nestedPa); + copyCustomEditorsTo(nestedPa, canonicalName); + this.nestedPropertyAccessors.put(canonicalName, nestedPa); + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Using cached nested property accessor for property '" + canonicalName + "'"); + } + } + return nestedPa; + } + + private Object setDefaultValue(String propertyName) { + PropertyTokenHolder tokens = new PropertyTokenHolder(); + tokens.actualName = propertyName; + tokens.canonicalName = propertyName; + return setDefaultValue(tokens); + } + + private Object setDefaultValue(PropertyTokenHolder tokens) { + PropertyValue pv = createDefaultPropertyValue(tokens); + setPropertyValue(tokens, pv); + return getPropertyValue(tokens); + } + + private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { + TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName); + Class type = desc.getType(); + if (type == null) { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Could not determine property type for auto-growing a default value"); + } + Object defaultValue = newValue(type, desc, tokens.canonicalName); + return new PropertyValue(tokens.canonicalName, defaultValue); + } + + private Object newValue(Class type, TypeDescriptor desc, String name) { + try { + if (type.isArray()) { + Class componentType = type.getComponentType(); + // TODO - only handles 2-dimensional arrays + if (componentType.isArray()) { + Object array = Array.newInstance(componentType, 1); + Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0)); + return array; + } + else { + return Array.newInstance(componentType, 0); + } + } + else if (Collection.class.isAssignableFrom(type)) { + TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null); + return CollectionFactory.createCollection(type, (elementDesc != null ? elementDesc.getType() : null), 16); + } + else if (Map.class.isAssignableFrom(type)) { + TypeDescriptor keyDesc = (desc != null ? desc.getMapKeyTypeDescriptor() : null); + return CollectionFactory.createMap(type, (keyDesc != null ? keyDesc.getType() : null), 16); + } + else { + return BeanUtils.instantiate(type); + } + } + catch (Exception ex) { + // TODO: Root cause exception context is lost here; just exception message preserved. + // Should we throw another exception type that preserves context instead? + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, + "Could not instantiate property type [" + type.getName() + "] to auto-grow nested property path: " + ex); + } + } + + /** + * Parse the given property name into the corresponding property name tokens. + * @param propertyName the property name to parse + * @return representation of the parsed property tokens + */ + private PropertyTokenHolder getPropertyNameTokens(String propertyName) { + PropertyTokenHolder tokens = new PropertyTokenHolder(); + String actualName = null; + List keys = new ArrayList(2); + int searchIndex = 0; + while (searchIndex != -1) { + int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex); + searchIndex = -1; + if (keyStart != -1) { + int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length()); + if (keyEnd != -1) { + if (actualName == null) { + actualName = propertyName.substring(0, keyStart); + } + String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); + if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { + key = key.substring(1, key.length() - 1); + } + keys.add(key); + searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); + } + } + } + tokens.actualName = (actualName != null ? actualName : propertyName); + tokens.canonicalName = tokens.actualName; + if (!keys.isEmpty()) { + tokens.canonicalName += + PROPERTY_KEY_PREFIX + + StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) + + PROPERTY_KEY_SUFFIX; + tokens.keys = StringUtils.toStringArray(keys); + } + return tokens; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getName()); + if (this.object != null) { + sb.append(": wrapping object [").append(ObjectUtils.identityToString(this.object)).append("]"); + } + else { + sb.append(": no wrapped object set"); + } + return sb.toString(); + } + + + /** + * Handle a given property. + */ + protected abstract static class PropertyHandler { + + private final Class propertyType; + + private final boolean readable; + + private final boolean writable; + + public PropertyHandler(Class propertyType, boolean readable, boolean writable) { + this.propertyType = propertyType; + this.readable = readable; + this.writable = writable; + } + + public Class getPropertyType() { + return this.propertyType; + } + + public boolean isReadable() { + return this.readable; + } + + public boolean isWritable() { + return this.writable; + } + + public abstract TypeDescriptor toTypeDescriptor(); + + public abstract ResolvableType getResolvableType(); + + public Class getMapKeyType(int nestingLevel) { + return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0); + } + + public Class getMapValueType(int nestingLevel) { + return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1); + } + + public Class getCollectionType(int nestingLevel) { + return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); + } + + public abstract TypeDescriptor nested(int level); + + public abstract Object getValue() throws Exception; + + public abstract void setValue(Object object, Object value) throws Exception; + + } + + protected static class PropertyTokenHolder { + + public String canonicalName; + + public String actualName; + + public String[] keys; + } + + + /** + * Inner class to avoid a hard dependency on Java 8. + */ + @UsesJava8 + private static class OptionalUnwrapper { + + public static Object unwrap(Object optionalObject) { + Optional optional = (Optional) optionalObject; + Assert.isTrue(optional.isPresent(), "Optional value must be present"); + Object result = optional.get(); + Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported"); + return result; + } + + public static boolean isEmpty(Object optionalObject) { + return !((Optional) optionalObject).isPresent(); + } + } +} 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 45f8ed5ce8..078712179a 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-2015 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,164 +16,28 @@ package org.springframework.beans; -import java.beans.PropertyChangeEvent; -import java.lang.reflect.Array; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.UndeclaredThrowableException; -import java.security.PrivilegedActionException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.core.CollectionFactory; -import org.springframework.core.ResolvableType; -import org.springframework.core.convert.ConversionException; -import org.springframework.core.convert.ConverterNotFoundException; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.UsesJava8; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; /** - * Abstract implementation of the {@link ConfigurablePropertyAccessor} interface. - * Provides the necessary infrastructure for all typical use cases. - * - *

This accessor will convert collection and array values to the corresponding - * target collections or arrays, if necessary. Custom property editors that deal - * with collections or arrays can either be written via PropertyEditor's - * {@code setValue}, or against a comma-delimited String via {@code setAsText}, - * as String arrays are converted in such a format if the array itself is not - * assignable. + * Abstract implementation of the {@link PropertyAccessor} interface. + * Provides base implementations of all convenience methods, with the + * implementation of actual property access left to subclasses. * - * @author Rod Johnson * @author Juergen Hoeller - * @author Rob Harrop * @author Stephane Nicoll * @since 2.0 - * @see #registerCustomEditor - * @see #setPropertyValues - * @see #setPropertyValue * @see #getPropertyValue - * @see #getPropertyType - * @see BeanWrapper - * @see PropertyEditorRegistrySupport + * @see #setPropertyValue */ public abstract class AbstractPropertyAccessor extends TypeConverterSupport implements ConfigurablePropertyAccessor { - /** - * We'll create a lot of these objects, so we don't want a new logger every time. - */ - private static final Log logger = LogFactory.getLog(AbstractPropertyAccessor.class); - - private static Class javaUtilOptionalClass = null; - - static { - try { - javaUtilOptionalClass = - ClassUtils.forName("java.util.Optional", AbstractPropertyAccessor.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - // Java 8 not available - Optional references simply not supported then. - } - } - private boolean extractOldValueForEditor = false; private boolean autoGrowNestedPaths = false; - private int autoGrowCollectionLimit = Integer.MAX_VALUE; - - /** The wrapped object */ - private Object object; - - private String nestedPath = ""; - - private Object rootObject; - - /** - * Map with cached nested Accessors: nested path -> Accessor instance. - */ - private Map nestedPropertyAccessors; - - /** - * Create new empty accessor. Wrapped instance needs to be set afterwards. - * Registers default editors. - * @see #setWrappedInstance - */ - protected AbstractPropertyAccessor() { - this(true); - } - - /** - * Create new empty accessor. Wrapped instance needs to be set afterwards. - * @param registerDefaultEditors whether to register default editors - * (can be suppressed if the accessor won't need any type conversion) - * @see #setWrappedInstance - */ - protected AbstractPropertyAccessor(boolean registerDefaultEditors) { - if (registerDefaultEditors) { - registerDefaultEditors(); - } - this.typeConverterDelegate = new TypeConverterDelegate(this); - } - - /** - * Create new accessor for the given object. - * @param object object wrapped by this accessor - */ - protected AbstractPropertyAccessor(Object object) { - registerDefaultEditors(); - setWrappedInstance(object); - } - - /** - * Create new accessor, wrapping a new instance of the specified class. - * @param clazz class to instantiate and wrap - */ - protected AbstractPropertyAccessor(Class clazz) { - registerDefaultEditors(); - setWrappedInstance(BeanUtils.instantiateClass(clazz)); - } - - /** - * Create new accessor for the given object, - * registering a nested path that the object is in. - * @param object object wrapped by this accessor - * @param nestedPath the nested path of the object - * @param rootObject the root object at the top of the path - */ - protected AbstractPropertyAccessor(Object object, String nestedPath, Object rootObject) { - registerDefaultEditors(); - setWrappedInstance(object, nestedPath, rootObject); - } - - /** - * Create new accessor for the given object, - * registering a nested path that the object is in. - * @param object object wrapped by this accessor - * @param nestedPath the nested path of the object - * @param parent the containing accessor (must not be {@code null}) - */ - protected AbstractPropertyAccessor(Object object, String nestedPath, AbstractPropertyAccessor parent) { - setWrappedInstance(object, nestedPath, parent.getWrappedInstance()); - setExtractOldValueForEditor(parent.isExtractOldValueForEditor()); - setAutoGrowNestedPaths(parent.isAutoGrowNestedPaths()); - setAutoGrowCollectionLimit(parent.getAutoGrowCollectionLimit()); - setConversionService(parent.getConversionService()); - } - @Override public void setExtractOldValueForEditor(boolean extractOldValueForEditor) { @@ -195,127 +59,10 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl return this.autoGrowNestedPaths; } - /** - * Specify a limit for array and collection auto-growing. - *

Default is unlimited on a plain accessor. - */ - public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { - this.autoGrowCollectionLimit = autoGrowCollectionLimit; - } - - /** - * Return the limit for array and collection auto-growing. - */ - public int getAutoGrowCollectionLimit() { - return this.autoGrowCollectionLimit; - } - - /** - * Switch the target object, replacing the cached introspection results only - * if the class of the new object is different to that of the replaced object. - * @param object the new target object - */ - public void setWrappedInstance(Object object) { - setWrappedInstance(object, "", null); - } - - /** - * Switch the target object, replacing the cached introspection results only - * if the class of the new object is different to that of the replaced object. - * @param object the new target object - * @param nestedPath the nested path of the object - * @param rootObject the root object at the top of the path - */ - public void setWrappedInstance(Object object, String nestedPath, Object rootObject) { - Assert.notNull(object, "Bean object must not be null"); - if (object.getClass().equals(javaUtilOptionalClass)) { - this.object = OptionalUnwrapper.unwrap(object); - } - else { - this.object = object; - } - this.nestedPath = (nestedPath != null ? nestedPath : ""); - this.rootObject = (!"".equals(this.nestedPath) ? rootObject : this.object); - this.nestedPropertyAccessors = null; - this.typeConverterDelegate = new TypeConverterDelegate(this, this.object); - } - - public final Object getWrappedInstance() { - return this.object; - } - - public final Class getWrappedClass() { - return (this.object != null ? this.object.getClass() : null); - } - - /** - * Return the nested path of the object wrapped by this accessor. - */ - public final String getNestedPath() { - return this.nestedPath; - } - - /** - * Return the root object at the top of the path of this accessor. - * @see #getNestedPath - */ - public final Object getRootInstance() { - return this.rootObject; - } - - /** - * Return the class of the root object at the top of the path of this accessor. - * @see #getNestedPath - */ - public final Class getRootClass() { - return (this.rootObject != null ? this.rootObject.getClass() : null); - } - - /** - * Actually set a property value. - * @param propertyName name of the property to set value of - * @param value the new value - * @throws InvalidPropertyException if there is no such property or - * if the property isn't writable - * @throws PropertyAccessException if the property was valid but the - * accessor method failed or a type mismatch occured - */ - @Override - public void setPropertyValue(String propertyName, Object value) throws BeansException { - AbstractPropertyAccessor nestedPa; - try { - nestedPa = getPropertyAccessorForPropertyPath(propertyName); - } - catch (NotReadablePropertyException ex) { - throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, - "Nested property in path '" + propertyName + "' does not exist", ex); - } - PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); - nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value)); - } @Override public void setPropertyValue(PropertyValue pv) throws BeansException { - PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens; - if (tokens == null) { - String propertyName = pv.getName(); - AbstractPropertyAccessor nestedPa; - try { - nestedPa = getPropertyAccessorForPropertyPath(propertyName); - } - catch (NotReadablePropertyException ex) { - throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, - "Nested property in path '" + propertyName + "' does not exist", ex); - } - tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); - if (nestedPa == this) { - pv.getOriginalPropertyValue().resolvedTokens = tokens; - } - nestedPa.setPropertyValue(tokens, pv); - } - else { - setPropertyValue(tokens, pv); - } + setPropertyValue(pv.getName(), pv.getValue()); } @Override @@ -375,325 +122,13 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl } } - @SuppressWarnings("unchecked") - protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { - String propertyName = tokens.canonicalName; - String actualName = tokens.actualName; - - if (tokens.keys != null) { - // Apply indexes and map keys: fetch value for all keys but the last one. - PropertyTokenHolder getterTokens = new PropertyTokenHolder(); - getterTokens.canonicalName = tokens.canonicalName; - getterTokens.actualName = tokens.actualName; - getterTokens.keys = new String[tokens.keys.length - 1]; - System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1); - Object propValue; - try { - propValue = getPropertyValue(getterTokens); - } - catch (NotReadablePropertyException ex) { - throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, - "Cannot access indexed value in property referenced " + - "in indexed property path '" + propertyName + "'", ex); - } - // Set value for last key. - String key = tokens.keys[tokens.keys.length - 1]; - if (propValue == null) { - // null map value case - if (isAutoGrowNestedPaths()) { - // TODO: cleanup, this is pretty hacky - int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); - getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex); - propValue = setDefaultValue(getterTokens); - } - else { - throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, - "Cannot access indexed value in property referenced " + - "in indexed property path '" + propertyName + "': returned null"); - } - } - if (propValue.getClass().isArray()) { - PropertyHandler ph = getLocalPropertyHandler(actualName); - Class requiredType = propValue.getClass().getComponentType(); - int arrayIndex = Integer.parseInt(key); - Object oldValue = null; - try { - if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) { - oldValue = Array.get(propValue, arrayIndex); - } - Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(), - requiredType, ph.nested(tokens.keys.length)); - int length = Array.getLength(propValue); - if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { - Class componentType = propValue.getClass().getComponentType(); - Object newArray = Array.newInstance(componentType, arrayIndex + 1); - System.arraycopy(propValue, 0, newArray, 0, length); - setPropertyValue(actualName, newArray); - propValue = getPropertyValue(actualName); - } - Array.set(propValue, arrayIndex, convertedValue); - } - catch (IndexOutOfBoundsException ex) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Invalid array index in property path '" + propertyName + "'", ex); - } - } - else if (propValue instanceof List) { - PropertyHandler ph = getPropertyHandler(actualName); - Class requiredType = ph.getCollectionType(tokens.keys.length); - List list = (List) propValue; - int index = Integer.parseInt(key); - Object oldValue = null; - if (isExtractOldValueForEditor() && index < list.size()) { - oldValue = list.get(index); - } - Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(), - requiredType, ph.nested(tokens.keys.length)); - int size = list.size(); - if (index >= size && index < this.autoGrowCollectionLimit) { - for (int i = size; i < index; i++) { - try { - list.add(null); - } - catch (NullPointerException ex) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Cannot set element with index " + index + " in List of size " + - size + ", accessed using property path '" + propertyName + - "': List does not support filling up gaps with null elements"); - } - } - list.add(convertedValue); - } - else { - try { - list.set(index, convertedValue); - } - catch (IndexOutOfBoundsException ex) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Invalid list index in property path '" + propertyName + "'", ex); - } - } - } - else if (propValue instanceof Map) { - PropertyHandler ph = getLocalPropertyHandler(actualName); - Class mapKeyType = ph.getMapKeyType(tokens.keys.length); - Class mapValueType = ph.getMapValueType(tokens.keys.length); - Map map = (Map) propValue; - // IMPORTANT: Do not pass full property name in here - property editors - // must not kick in for map keys but rather only for map values. - TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); - Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); - Object oldValue = null; - if (isExtractOldValueForEditor()) { - oldValue = map.get(convertedMapKey); - } - // Pass full property name and old value in here, since we want full - // conversion ability for map values. - Object convertedMapValue = convertIfNecessary(propertyName, oldValue, pv.getValue(), - mapValueType, ph.nested(tokens.keys.length)); - map.put(convertedMapKey, convertedMapValue); - } - else { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Property referenced in indexed property path '" + propertyName + - "' is neither an array nor a List nor a Map; returned value was [" + propValue + "]"); - } - } - - else { - PropertyHandler ph = getLocalPropertyHandler(actualName); - if (ph == null || !ph.isWritable()) { - if (pv.isOptional()) { - if (logger.isDebugEnabled()) { - logger.debug("Ignoring optional value for property '" + actualName + - "' - property not found on bean class [" + getRootClass().getName() + "]"); - } - return; - } - else { - throw createNotWritablePropertyException(propertyName); - } - } - Object oldValue = null; - try { - Object originalValue = pv.getValue(); - Object valueToApply = originalValue; - if (!Boolean.FALSE.equals(pv.conversionNecessary)) { - if (pv.isConverted()) { - valueToApply = pv.getConvertedValue(); - } - else { - if (isExtractOldValueForEditor() && ph.isReadable()) { - try { - oldValue = ph.getValue(); - } - catch (Exception ex) { - if (ex instanceof PrivilegedActionException) { - ex = ((PrivilegedActionException) ex).getException(); - } - if (logger.isDebugEnabled()) { - logger.debug("Could not read previous value of property '" + - this.nestedPath + propertyName + "'", ex); - } - } - } - valueToApply = convertForProperty( - propertyName, oldValue, originalValue, ph.toTypeDescriptor()); - } - pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue); - } - ph.setValue(object, valueToApply); - } - catch (TypeMismatchException ex) { - throw ex; - } - catch (InvocationTargetException ex) { - PropertyChangeEvent propertyChangeEvent = - new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); - if (ex.getTargetException() instanceof ClassCastException) { - throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), ex.getTargetException()); - } - else { - Throwable cause = ex.getTargetException(); - if (cause instanceof UndeclaredThrowableException) { - // May happen e.g. with Groovy-generated methods - cause = cause.getCause(); - } - throw new MethodInvocationException(propertyChangeEvent, cause); - } - } - catch (Exception ex) { - PropertyChangeEvent pce = - new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); - throw new MethodInvocationException(pce, ex); - } - } - } - - @Override - public Class getPropertyType(String propertyName) throws BeansException { - try { - PropertyHandler ph = getPropertyHandler(propertyName); - if (ph != null) { - return ph.getPropertyType(); - } - else { - // Maybe an indexed/mapped property... - Object value = getPropertyValue(propertyName); - if (value != null) { - return value.getClass(); - } - // Check to see if there is a custom editor, - // which might give an indication on the desired target type. - Class editorType = guessPropertyTypeFromEditors(propertyName); - if (editorType != null) { - return editorType; - } - } - } - catch (InvalidPropertyException ex) { - // Consider as not determinable. - } - return null; - } + // Redefined with public visibility. @Override - public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { - try { - AbstractPropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); - String finalPath = getFinalPath(nestedPa, propertyName); - PropertyTokenHolder tokens = getPropertyNameTokens(finalPath); - PropertyHandler ph = nestedPa.getLocalPropertyHandler(tokens.actualName); - if (ph != null) { - if (tokens.keys != null) { - if (ph.isReadable() || ph.isWritable()) { - return ph.nested(tokens.keys.length); - } - } - else { - if (ph.isReadable() || ph.isWritable()) { - return ph.toTypeDescriptor(); - } - } - } - } - catch (InvalidPropertyException ex) { - // Consider as not determinable. - } + public Class getPropertyType(String propertyPath) { return null; } - @Override - public boolean isReadableProperty(String propertyName) { - try { - PropertyHandler ph = getPropertyHandler(propertyName); - if (ph != null) { - return ph.isReadable(); - } - else { - // Maybe an indexed/mapped property... - getPropertyValue(propertyName); - return true; - } - } - catch (InvalidPropertyException ex) { - // Cannot be evaluated, so can't be readable. - } - return false; - } - - @Override - public boolean isWritableProperty(String propertyName) { - try { - PropertyHandler ph = getPropertyHandler(propertyName); - if (ph != null) { - return ph.isWritable(); - } - else { - // Maybe an indexed/mapped property... - getPropertyValue(propertyName); - return true; - } - } - catch (InvalidPropertyException ex) { - // Cannot be evaluated, so can't be writable. - } - return false; - } - - private Object convertIfNecessary(String propertyName, Object oldValue, Object newValue, Class requiredType, - TypeDescriptor td) throws TypeMismatchException { - try { - return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td); - } - catch (ConverterNotFoundException ex) { - PropertyChangeEvent pce = - new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); - throw new ConversionNotSupportedException(pce, td.getType(), ex); - } - catch (ConversionException ex) { - PropertyChangeEvent pce = - new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); - throw new TypeMismatchException(pce, requiredType, ex); - } - catch (IllegalStateException ex) { - PropertyChangeEvent pce = - new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); - throw new ConversionNotSupportedException(pce, requiredType, ex); - } - catch (IllegalArgumentException ex) { - PropertyChangeEvent pce = - new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); - throw new TypeMismatchException(pce, requiredType, ex); - } - } - - protected Object convertForProperty(String propertyName, Object oldValue, Object newValue, TypeDescriptor td) - throws TypeMismatchException { - - return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td); - } - /** * Actually get the value of a property. * @param propertyName name of the property to get the value of @@ -704,459 +139,18 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl * accessor method failed */ @Override - public Object getPropertyValue(String propertyName) throws BeansException { - AbstractPropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); - PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); - return nestedPa.getPropertyValue(tokens); - } - - @SuppressWarnings("unchecked") - protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { - String propertyName = tokens.canonicalName; - String actualName = tokens.actualName; - PropertyHandler ph = getLocalPropertyHandler(actualName); - if (ph == null || !ph.isReadable()) { - throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName); - } - try { - Object value = ph.getValue(); - if (tokens.keys != null) { - if (value == null) { - if (isAutoGrowNestedPaths()) { - value = setDefaultValue(tokens.actualName); - } - else { - throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, - "Cannot access indexed value of property referenced in indexed " + - "property path '" + propertyName + "': returned null"); - } - } - String indexedPropertyName = tokens.actualName; - // apply indexes and map keys - for (int i = 0; i < tokens.keys.length; i++) { - String key = tokens.keys[i]; - if (value == null) { - throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, - "Cannot access indexed value of property referenced in indexed " + - "property path '" + propertyName + "': returned null"); - } - else if (value.getClass().isArray()) { - int index = Integer.parseInt(key); - value = growArrayIfNecessary(value, index, indexedPropertyName); - value = Array.get(value, index); - } - else if (value instanceof List) { - int index = Integer.parseInt(key); - List list = (List) value; - growCollectionIfNecessary(list, index, indexedPropertyName, ph, i + 1); - value = list.get(index); - } - else if (value instanceof Set) { - // Apply index to Iterator in case of a Set. - Set set = (Set) value; - int index = Integer.parseInt(key); - if (index < 0 || index >= set.size()) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Cannot get element with index " + index + " from Set of size " + - set.size() + ", accessed using property path '" + propertyName + "'"); - } - Iterator it = set.iterator(); - for (int j = 0; it.hasNext(); j++) { - Object elem = it.next(); - if (j == index) { - value = elem; - break; - } - } - } - else if (value instanceof Map) { - Map map = (Map) value; - Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); - // IMPORTANT: Do not pass full property name in here - property editors - // must not kick in for map keys but rather only for map values. - TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); - Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); - value = map.get(convertedMapKey); - } - else { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Property referenced in indexed property path '" + propertyName + - "' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]"); - } - indexedPropertyName += PROPERTY_KEY_PREFIX + key + PROPERTY_KEY_SUFFIX; - } - } - return value; - } - catch (IndexOutOfBoundsException ex) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Index of out of bounds in property path '" + propertyName + "'", ex); - } - catch (NumberFormatException ex) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Invalid index in property path '" + propertyName + "'", ex); - } - catch (TypeMismatchException ex) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Invalid index in property path '" + propertyName + "'", ex); - } - catch (InvocationTargetException ex) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Getter for property '" + actualName + "' threw exception", ex); - } - catch (Exception ex) { - throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, - "Illegal attempt to get property '" + actualName + "' threw exception", ex); - } - } - - - /** - * Return the {@link PropertyHandler} for the specified {@code propertyName}, navigating - * if necessary. Return {@code null} if not found rather than throwing an exception. - * @param propertyName the property to obtain the descriptor for - * @return the property descriptor for the specified property, - * or {@code null} if not found - * @throws BeansException in case of introspection failure - */ - protected PropertyHandler getPropertyHandler(String propertyName) throws BeansException { - Assert.notNull(propertyName, "Property name must not be null"); - AbstractPropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); - return nestedPa.getLocalPropertyHandler(getFinalPath(nestedPa, propertyName)); - } - - /** - * Return a {@link PropertyHandler} for the specified local {@code propertyName}. Only - * used to reach a property available in the current context. - * @param propertyName the name of a local property - * @return the handler for that property or {@code null} if it has not been found - */ - protected abstract PropertyHandler getLocalPropertyHandler(String propertyName); - - /** - * Create a new nested property accessor instance. - * Can be overridden in subclasses to create a PropertyAccessor subclass. - * @param object object wrapped by this PropertyAccessor - * @param nestedPath the nested path of the object - * @return the nested PropertyAccessor instance - */ - protected abstract AbstractPropertyAccessor newNestedPropertyAccessor(Object object, String nestedPath); - - /** - * Create a {@link NotWritablePropertyException} for the specified property. - */ - protected abstract NotWritablePropertyException createNotWritablePropertyException(String propertyName); - - - private Object growArrayIfNecessary(Object array, int index, String name) { - if (!isAutoGrowNestedPaths()) { - return array; - } - int length = Array.getLength(array); - if (index >= length && index < this.autoGrowCollectionLimit) { - Class componentType = array.getClass().getComponentType(); - Object newArray = Array.newInstance(componentType, index + 1); - System.arraycopy(array, 0, newArray, 0, length); - for (int i = length; i < Array.getLength(newArray); i++) { - Array.set(newArray, i, newValue(componentType, null, name)); - } - // TODO this is not efficient because conversion may create a copy ... set directly because we know it is assignable. - setPropertyValue(name, newArray); - return getPropertyValue(name); - } - else { - return array; - } - } - - private void growCollectionIfNecessary(Collection collection, int index, String name, - PropertyHandler ph, int nestingLevel) { - - if (!isAutoGrowNestedPaths()) { - return; - } - int size = collection.size(); - if (index >= size && index < this.autoGrowCollectionLimit) { - Class elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); - if (elementType != null) { - for (int i = collection.size(); i < index + 1; i++) { - collection.add(newValue(elementType, null, name)); - } - } - } - } + public abstract Object getPropertyValue(String propertyName) throws BeansException; /** - * Get the last component of the path. Also works if not nested. - * @param pa property accessor to work on - * @param nestedPath property path we know is nested - * @return last component of the path (the property on the target bean) - */ - private String getFinalPath(AbstractPropertyAccessor pa, String nestedPath) { - if (pa == this) { - return nestedPath; - } - return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1); - } - - /** - * Recursively navigate to return a property accessor for the nested property path. - * @param propertyPath property property path, which may be nested - * @return a property accessor for the target bean - */ - @SuppressWarnings("unchecked") // avoid nested generic - protected AbstractPropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) { - int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath); - // Handle nested properties recursively. - if (pos > -1) { - String nestedProperty = propertyPath.substring(0, pos); - String nestedPath = propertyPath.substring(pos + 1); - AbstractPropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty); - return nestedPa.getPropertyAccessorForPropertyPath(nestedPath); - } - else { - return this; - } - } - - /** - * Retrieve a Property accessor for the given nested property. - * Create a new one if not found in the cache. - *

Note: Caching nested PropertyAccessors is necessary now, - * to keep registered custom editors for nested properties. - * @param nestedProperty property to create the PropertyAccessor for - * @return the PropertyAccessor instance, either cached or newly created - */ - private AbstractPropertyAccessor getNestedPropertyAccessor(String nestedProperty) { - if (this.nestedPropertyAccessors == null) { - this.nestedPropertyAccessors = new HashMap(); - } - // Get value of bean property. - PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); - String canonicalName = tokens.canonicalName; - Object value = getPropertyValue(tokens); - if (value == null || (value.getClass().equals(javaUtilOptionalClass) && OptionalUnwrapper.isEmpty(value))) { - if (isAutoGrowNestedPaths()) { - value = setDefaultValue(tokens); - } - else { - throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName); - } - } - - // Lookup cached sub-PropertyAccessor, create new one if not found. - AbstractPropertyAccessor nestedPa = this.nestedPropertyAccessors.get(canonicalName); - if (nestedPa == null || nestedPa.getWrappedInstance() != - (value.getClass().equals(javaUtilOptionalClass) ? OptionalUnwrapper.unwrap(value) : value)) { - if (logger.isTraceEnabled()) { - logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'"); - } - nestedPa = newNestedPropertyAccessor(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR); - // Inherit all type-specific PropertyEditors. - copyDefaultEditorsTo(nestedPa); - copyCustomEditorsTo(nestedPa, canonicalName); - this.nestedPropertyAccessors.put(canonicalName, nestedPa); - } - else { - if (logger.isTraceEnabled()) { - logger.trace("Using cached nested property accessor for property '" + canonicalName + "'"); - } - } - return nestedPa; - } - - private Object setDefaultValue(String propertyName) { - PropertyTokenHolder tokens = new PropertyTokenHolder(); - tokens.actualName = propertyName; - tokens.canonicalName = propertyName; - return setDefaultValue(tokens); - } - - private Object setDefaultValue(PropertyTokenHolder tokens) { - PropertyValue pv = createDefaultPropertyValue(tokens); - setPropertyValue(tokens, pv); - return getPropertyValue(tokens); - } - - private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { - TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName); - Class type = desc.getType(); - if (type == null) { - throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, - "Could not determine property type for auto-growing a default value"); - } - Object defaultValue = newValue(type, desc, tokens.canonicalName); - return new PropertyValue(tokens.canonicalName, defaultValue); - } - - private Object newValue(Class type, TypeDescriptor desc, String name) { - try { - if (type.isArray()) { - Class componentType = type.getComponentType(); - // TODO - only handles 2-dimensional arrays - if (componentType.isArray()) { - Object array = Array.newInstance(componentType, 1); - Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0)); - return array; - } - else { - return Array.newInstance(componentType, 0); - } - } - else if (Collection.class.isAssignableFrom(type)) { - TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null); - return CollectionFactory.createCollection(type, (elementDesc != null ? elementDesc.getType() : null), 16); - } - else if (Map.class.isAssignableFrom(type)) { - TypeDescriptor keyDesc = (desc != null ? desc.getMapKeyTypeDescriptor() : null); - return CollectionFactory.createMap(type, (keyDesc != null ? keyDesc.getType() : null), 16); - } - else { - return BeanUtils.instantiate(type); - } - } - catch (Exception ex) { - // TODO: Root cause exception context is lost here; just exception message preserved. - // Should we throw another exception type that preserves context instead? - throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, - "Could not instantiate property type [" + type.getName() + "] to auto-grow nested property path: " + ex); - } - } - - /** - * Parse the given property name into the corresponding property name tokens. - * @param propertyName the property name to parse - * @return representation of the parsed property tokens + * Actually set a property value. + * @param propertyName name of the property to set value of + * @param value the new value + * @throws InvalidPropertyException if there is no such property or + * if the property isn't writable + * @throws PropertyAccessException if the property was valid but the + * accessor method failed or a type mismatch occured */ - private PropertyTokenHolder getPropertyNameTokens(String propertyName) { - PropertyTokenHolder tokens = new PropertyTokenHolder(); - String actualName = null; - List keys = new ArrayList(2); - int searchIndex = 0; - while (searchIndex != -1) { - int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex); - searchIndex = -1; - if (keyStart != -1) { - int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length()); - if (keyEnd != -1) { - if (actualName == null) { - actualName = propertyName.substring(0, keyStart); - } - String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); - if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { - key = key.substring(1, key.length() - 1); - } - keys.add(key); - searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); - } - } - } - tokens.actualName = (actualName != null ? actualName : propertyName); - tokens.canonicalName = tokens.actualName; - if (!keys.isEmpty()) { - tokens.canonicalName += - PROPERTY_KEY_PREFIX + - StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) + - PROPERTY_KEY_SUFFIX; - tokens.keys = StringUtils.toStringArray(keys); - } - return tokens; - } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(getClass().getName()); - if (this.object != null) { - sb.append(": wrapping object [").append(ObjectUtils.identityToString(this.object)).append("]"); - } - else { - sb.append(": no wrapped object set"); - } - return sb.toString(); - } - - - /** - * Handle a given property. - */ - protected abstract static class PropertyHandler { - - private final Class propertyType; - - private final boolean readable; - - private final boolean writable; - - public PropertyHandler(Class propertyType, boolean readable, boolean writable) { - this.propertyType = propertyType; - this.readable = readable; - this.writable = writable; - } - - public Class getPropertyType() { - return this.propertyType; - } - - public boolean isReadable() { - return this.readable; - } - - public boolean isWritable() { - return this.writable; - } - - public abstract TypeDescriptor toTypeDescriptor(); - - public abstract ResolvableType getResolvableType(); - - public Class getMapKeyType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0); - } - - public Class getMapValueType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1); - } - - public Class getCollectionType(int nestingLevel) { - return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); - } - - public abstract TypeDescriptor nested(int level); - - public abstract Object getValue() throws Exception; - - public abstract void setValue(Object object, Object value) throws Exception; - - } - - protected static class PropertyTokenHolder { - - public String canonicalName; - - public String actualName; - - public String[] keys; - } - - - /** - * Inner class to avoid a hard dependency on Java 8. - */ - @UsesJava8 - private static class OptionalUnwrapper { - - public static Object unwrap(Object optionalObject) { - Optional optional = (Optional) optionalObject; - Assert.isTrue(optional.isPresent(), "Optional value must be present"); - Object result = optional.get(); - Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported"); - return result; - } - - public static boolean isEmpty(Object optionalObject) { - return !((Optional) optionalObject).isPresent(); - } - } - + public abstract void setPropertyValue(String propertyName, Object value) throws BeansException; } 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 1d72ff9a9c..637da375bc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -60,7 +60,7 @@ import org.springframework.util.Assert; * @see BeanWrapper * @see PropertyEditorRegistrySupport */ -public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWrapper { +public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper { /** * Cached introspections results for this object, to prevent encountering 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 44e43d75f4..687d55d46d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java @@ -44,7 +44,7 @@ import org.springframework.util.ReflectionUtils; * @see org.springframework.validation.DirectFieldBindingResult * @see org.springframework.validation.DataBinder#initDirectFieldAccess() */ -public class DirectFieldAccessor extends AbstractPropertyAccessor { +public class DirectFieldAccessor extends AbstractNestablePropertyAccessor { private final Map fieldMap = new HashMap(); diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractConfigurablePropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java similarity index 99% rename from spring-beans/src/test/java/org/springframework/beans/AbstractConfigurablePropertyAccessorTests.java rename to spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java index 7fac2cebeb..0180fbc42d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractConfigurablePropertyAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -76,7 +76,7 @@ import static org.junit.Assert.*; * @author Dave Syer * @author Stephane Nicoll */ -public abstract class AbstractConfigurablePropertyAccessorTests { +public abstract class AbstractPropertyAccessorTests { @Rule public final ExpectedException thrown = ExpectedException.none(); @@ -1035,7 +1035,7 @@ public abstract class AbstractConfigurablePropertyAccessorTests { @Test public void setPrimitiveArrayPropertyLargeMatching() { Assume.group(TestGroup.PERFORMANCE); - Assume.notLogging(LogFactory.getLog(AbstractConfigurablePropertyAccessorTests.class)); + Assume.notLogging(LogFactory.getLog(AbstractPropertyAccessorTests.class)); PrimitiveArrayBean target = new PrimitiveArrayBean(); AbstractPropertyAccessor accessor = createAccessor(target); 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 24311b0e43..a46275f972 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java @@ -36,7 +36,7 @@ import static org.junit.Assert.*; * @author Chris Beams * @author Dave Syer */ -public final class BeanWrapperTests extends AbstractConfigurablePropertyAccessorTests { +public final class BeanWrapperTests extends AbstractPropertyAccessorTests { @Override protected BeanWrapperImpl createAccessor(Object target) { 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 543f12f406..4145e7bb9b 100644 --- a/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java @@ -29,7 +29,7 @@ import static org.junit.Assert.*; * @author Chris Beams * @@author Stephane Nicoll */ -public class DirectFieldAccessorTests extends AbstractConfigurablePropertyAccessorTests { +public class DirectFieldAccessorTests extends AbstractPropertyAccessorTests { @Override protected DirectFieldAccessor createAccessor(Object target) {