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