/*
 * Copyright 2002-2020 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
 *
 *      https://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.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
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.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

A basic 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 setValue, or against a comma-delimited String via setAsText, as String arrays are converted in such a format if the array itself is not assignable.

Author:Juergen Hoeller, Stephane Nicoll, Rod Johnson, Rob Harrop
See Also:
Since:4.2
/** * A basic {@link ConfigurablePropertyAccessor} that provides the necessary * infrastructure for all typical use cases. * * <p>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 Juergen Hoeller * @author Stephane Nicoll * @author Rod Johnson * @author Rob Harrop * @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.
/** * 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 int autoGrowCollectionLimit = Integer.MAX_VALUE; @Nullable Object wrappedObject; private String nestedPath = ""; @Nullable Object rootObject;
Map with cached nested Accessors: nested path -> Accessor instance.
/** Map with cached nested Accessors: nested path -> Accessor instance. */
@Nullable private Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors;
Create a new empty accessor. Wrapped instance needs to be set afterwards. Registers default editors.
See Also:
  • setWrappedInstance
/** * Create a new empty accessor. Wrapped instance needs to be set afterwards. * Registers default editors. * @see #setWrappedInstance */
protected AbstractNestablePropertyAccessor() { this(true); }
Create a new empty accessor. Wrapped instance needs to be set afterwards.
Params:
  • registerDefaultEditors – whether to register default editors (can be suppressed if the accessor won't need any type conversion)
See Also:
/** * Create a 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 a new accessor for the given object.
Params:
  • object – the object wrapped by this accessor
/** * Create a new accessor for the given object. * @param object the object wrapped by this accessor */
protected AbstractNestablePropertyAccessor(Object object) { registerDefaultEditors(); setWrappedInstance(object); }
Create a new accessor, wrapping a new instance of the specified class.
Params:
  • clazz – class to instantiate and wrap
/** * Create a 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 a new accessor for the given object, registering a nested path that the object is in.
Params:
  • object – the object wrapped by this accessor
  • nestedPath – the nested path of the object
  • rootObject – the root object at the top of the path
/** * Create a new accessor for the given object, * registering a nested path that the object is in. * @param object the 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 a new accessor for the given object, registering a nested path that the object is in.
Params:
  • object – the object wrapped by this accessor
  • nestedPath – the nested path of the object
  • parent – the containing accessor (must not be null)
/** * Create a new accessor for the given object, * registering a nested path that the object is in. * @param object the 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.

/** * Specify a limit for array and collection auto-growing. * <p>Default is unlimited on a plain accessor. */
public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { this.autoGrowCollectionLimit = autoGrowCollectionLimit; }
Return the limit for array and collection auto-growing.
/** * 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.
Params:
  • object – the new target object
/** * 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.
Params:
  • object – the new target object
  • nestedPath – the nested path of the object
  • rootObject – the root object at the top of the path
/** * 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, @Nullable String nestedPath, @Nullable Object rootObject) { this.wrappedObject = ObjectUtils.unwrapOptional(object); Assert.notNull(this.wrappedObject, "Target object must not be null"); this.nestedPath = (nestedPath != null ? nestedPath : ""); this.rootObject = (!this.nestedPath.isEmpty() ? rootObject : this.wrappedObject); this.nestedPropertyAccessors = null; this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject); } public final Object getWrappedInstance() { Assert.state(this.wrappedObject != null, "No wrapped object"); return this.wrappedObject; } public final Class<?> getWrappedClass() { return getWrappedInstance().getClass(); }
Return the nested path of the object wrapped by this accessor.
/** * 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 Also:
  • getNestedPath
/** * Return the root object at the top of the path of this accessor. * @see #getNestedPath */
public final Object getRootInstance() { Assert.state(this.rootObject != null, "No root object"); return this.rootObject; }
Return the class of the root object at the top of the path of this accessor.
See Also:
  • getNestedPath
/** * Return the class of the root object at the top of the path of this accessor. * @see #getNestedPath */
public final Class<?> getRootClass() { return getRootInstance().getClass(); } @Override public void setPropertyValue(String propertyName, @Nullable 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); } } protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { if (tokens.keys != null) { processKeyedProperty(tokens, pv); } else { processLocalProperty(tokens, pv); } } @SuppressWarnings("unchecked") private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) { Object propValue = getPropertyHoldingValue(tokens); PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); if (ph == null) { throw new InvalidPropertyException( getRootClass(), this.nestedPath + tokens.actualName, "No property handler found"); } Assert.state(tokens.keys != null, "No token keys"); String lastKey = tokens.keys[tokens.keys.length - 1]; if (propValue.getClass().isArray()) { Class<?> requiredType = propValue.getClass().getComponentType(); int arrayIndex = Integer.parseInt(lastKey); Object oldValue = null; try { if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) { oldValue = Array.get(propValue, arrayIndex); } Object convertedValue = convertIfNecessary(tokens.canonicalName, 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(tokens.actualName, newArray); propValue = getPropertyValue(tokens.actualName); } Array.set(propValue, arrayIndex, convertedValue); } catch (IndexOutOfBoundsException ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, "Invalid array index in property path '" + tokens.canonicalName + "'", ex); } } else if (propValue instanceof List) { Class<?> requiredType = ph.getCollectionType(tokens.keys.length); List<Object> list = (List<Object>) propValue; int index = Integer.parseInt(lastKey); Object oldValue = null; if (isExtractOldValueForEditor() && index < list.size()) { oldValue = list.get(index); } Object convertedValue = convertIfNecessary(tokens.canonicalName, 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 + tokens.canonicalName, "Cannot set element with index " + index + " in List of size " + size + ", accessed using property path '" + tokens.canonicalName + "': 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 + tokens.canonicalName, "Invalid list index in property path '" + tokens.canonicalName + "'", ex); } } } else if (propValue instanceof Map) { Class<?> mapKeyType = ph.getMapKeyType(tokens.keys.length); Class<?> mapValueType = ph.getMapValueType(tokens.keys.length); Map<Object, Object> map = (Map<Object, Object>) 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, lastKey, 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(tokens.canonicalName, oldValue, pv.getValue(), mapValueType, ph.nested(tokens.keys.length)); map.put(convertedMapKey, convertedMapValue); } else { throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, "Property referenced in indexed property path '" + tokens.canonicalName + "' is neither an array nor a List nor a Map; returned value was [" + propValue + "]"); } } private Object getPropertyHoldingValue(PropertyTokenHolder tokens) { // Apply indexes and map keys: fetch value for all keys but the last one. Assert.state(tokens.keys != null, "No token keys"); PropertyTokenHolder getterTokens = new PropertyTokenHolder(tokens.actualName); getterTokens.canonicalName = tokens.canonicalName; 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 + tokens.canonicalName, "Cannot access indexed value in property referenced " + "in indexed property path '" + tokens.canonicalName + "'", ex); } if (propValue == null) { // null map value case if (isAutoGrowNestedPaths()) { int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex); propValue = setDefaultValue(getterTokens); } else { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, "Cannot access indexed value in property referenced " + "in indexed property path '" + tokens.canonicalName + "': returned null"); } } return propValue; } private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) { PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); if (ph == null || !ph.isWritable()) { if (pv.isOptional()) { if (logger.isDebugEnabled()) { logger.debug("Ignoring optional value for property '" + tokens.actualName + "' - property not found on bean class [" + getRootClass().getName() + "]"); } return; } if (this.suppressNotWritablePropertyException) { // Optimization for common ignoreUnknown=true scenario since the // exception would be caught and swallowed higher up anyway... return; } throw createNotWritablePropertyException(tokens.canonicalName); } 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 + tokens.canonicalName + "'", ex); } } } valueToApply = convertForProperty( tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor()); } pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue); } ph.setValue(valueToApply); } catch (TypeMismatchException ex) { throw ex; } catch (InvocationTargetException ex) { PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent( getRootInstance(), this.nestedPath + tokens.canonicalName, 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( getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue()); throw new MethodInvocationException(pce, ex); } } @Override @Nullable 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 @Nullable 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; } @Nullable private Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<?> requiredType, @Nullable TypeDescriptor td) throws TypeMismatchException { Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate"); try { return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td); } catch (ConverterNotFoundException | IllegalStateException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(getRootInstance(), this.nestedPath + propertyName, oldValue, newValue); throw new ConversionNotSupportedException(pce, requiredType, ex); } catch (ConversionException | IllegalArgumentException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(getRootInstance(), this.nestedPath + propertyName, oldValue, newValue); throw new TypeMismatchException(pce, requiredType, ex); } } @Nullable protected Object convertForProperty( String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) throws TypeMismatchException { return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td); } @Override @Nullable public Object getPropertyValue(String propertyName) throws BeansException { AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); return nestedPa.getPropertyValue(tokens); } @SuppressWarnings("unchecked") @Nullable 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(new PropertyTokenHolder(tokens.actualName)); } else { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, "Cannot access indexed value of property referenced in indexed " + "property path '" + propertyName + "': returned null"); } } StringBuilder indexedPropertyName = new StringBuilder(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.toString()); value = Array.get(value, index); } else if (value instanceof List) { int index = Integer.parseInt(key); List<Object> list = (List<Object>) value; growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); value = list.get(index); } else if (value instanceof Set) { // Apply index to Iterator in case of a Set. Set<Object> set = (Set<Object>) 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<Object> 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<Object, Object> map = (Map<Object, Object>) 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.append(PROPERTY_KEY_PREFIX).append(key).append(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 | 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 PropertyHandler for the specified propertyName, navigating if necessary. Return null if not found rather than throwing an exception.
Params:
  • propertyName – the property to obtain the descriptor for
Throws:
Returns:the property descriptor for the specified property, or null if not found
/** * 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 */
@Nullable 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 PropertyHandler for the specified local propertyName. Only used to reach a property available in the current context.
Params:
  • propertyName – the name of a local property
Returns:the handler for that property, or null if it has not been found
/** * 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 */
@Nullable protected abstract PropertyHandler getLocalPropertyHandler(String propertyName);
Create a new nested property accessor instance. Can be overridden in subclasses to create a PropertyAccessor subclass.
Params:
  • object – the object wrapped by this PropertyAccessor
  • nestedPath – the nested path of the object
Returns:the nested PropertyAccessor instance
/** * Create a new nested property accessor instance. * Can be overridden in subclasses to create a PropertyAccessor subclass. * @param object the 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 NotWritablePropertyException for the specified property.
/** * 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)); } setPropertyValue(name, newArray); Object defaultValue = getPropertyValue(name); Assert.state(defaultValue != null, "Default value must not be null"); return defaultValue; } else { return array; } } private void growCollectionIfNecessary(Collection<Object> 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.
Params:
  • pa – property accessor to work on
  • nestedPath – property path we know is nested
Returns:last component of the path (the property on the target bean)
/** * 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) */
protected 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.
Params:
  • propertyPath – property path, which may be nested
Returns:a property accessor for the target bean
/** * Recursively navigate to return a property accessor for the nested property path. * @param propertyPath property path, which may be nested * @return a property accessor for the target bean */
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.

Params:
  • nestedProperty – property to create the PropertyAccessor for
Returns:the PropertyAccessor instance, either cached or newly created
/** * Retrieve a Property accessor for the given nested property. * Create a new one if not found in the cache. * <p>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 instanceof Optional && !((Optional<?>) value).isPresent())) { 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() != ObjectUtils.unwrapOptional(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(PropertyTokenHolder tokens) { PropertyValue pv = createDefaultPropertyValue(tokens); setPropertyValue(tokens, pv); Object defaultValue = getPropertyValue(tokens); Assert.state(defaultValue != null, "Default value must not be null"); return defaultValue; } private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName); if (desc == null) { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, "Could not determine property type for auto-growing a default value"); } Object defaultValue = newValue(desc.getType(), desc, tokens.canonicalName); return new PropertyValue(tokens.canonicalName, defaultValue); } private Object newValue(Class<?> type, @Nullable 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 { Constructor<?> ctor = type.getDeclaredConstructor(); if (Modifier.isPrivate(ctor.getModifiers())) { throw new IllegalAccessException("Auto-growing not allowed with private constructor: " + ctor); } return BeanUtils.instantiateClass(ctor); } } catch (Throwable ex) { 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.
Params:
  • propertyName – the property name to parse
Returns:representation of the parsed property tokens
/** * 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) { String actualName = null; List<String> 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 = getPropertyNameKeyEnd(propertyName, 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.length() > 1 && (key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { key = key.substring(1, key.length() - 1); } keys.add(key); searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); } } } PropertyTokenHolder tokens = new PropertyTokenHolder(actualName != null ? actualName : propertyName); 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; } private int getPropertyNameKeyEnd(String propertyName, int startIndex) { int unclosedPrefixes = 0; int length = propertyName.length(); for (int i = startIndex; i < length; i++) { switch (propertyName.charAt(i)) { case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: // The property name contains opening prefix(es)... unclosedPrefixes++; break; case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: if (unclosedPrefixes == 0) { // No unclosed prefix(es) in the property name (left) -> // this is the suffix we are looking for. return i; } else { // This suffix does not close the initial prefix but rather // just one that occurred within the property name. unclosedPrefixes--; } break; } } return -1; } @Override public String toString() { String className = getClass().getName(); if (this.wrappedObject == null) { return className + ": no wrapped object set"; } return className + ": wrapping object [" + ObjectUtils.identityToString(this.wrappedObject) + ']'; }
A handler for a specific property.
/** * A handler for a specific 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(); @Nullable public Class<?> getMapKeyType(int nestingLevel) { return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0); } @Nullable public Class<?> getMapValueType(int nestingLevel) { return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1); } @Nullable public Class<?> getCollectionType(int nestingLevel) { return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); } @Nullable public abstract TypeDescriptor nested(int level); @Nullable public abstract Object getValue() throws Exception; public abstract void setValue(@Nullable Object value) throws Exception; }
Holder class used to store property tokens.
/** * Holder class used to store property tokens. */
protected static class PropertyTokenHolder { public PropertyTokenHolder(String name) { this.actualName = name; this.canonicalName = name; } public String actualName; public String canonicalName; @Nullable public String[] keys; } }