net.yasion.common.core.bean.wrapper.impl.ExtendedBeanWrapperImpl.java Source code

Java tutorial

Introduction

Here is the source code for net.yasion.common.core.bean.wrapper.impl.ExtendedBeanWrapperImpl.java

Source

package net.yasion.common.core.bean.wrapper.impl;

/*
 * Copyright 2002-2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.beans.PropertyChangeEvent;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
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 net.yasion.common.core.bean.wrapper.CachedIntrospectionResults;
import net.yasion.common.core.bean.wrapper.GenericTypeAwarePropertyDescriptor;
import net.yasion.common.core.bean.wrapper.PropertyMatches;
import net.yasion.common.core.bean.wrapper.TypeConverterDelegate;
import net.yasion.common.utils.AfxBeanUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.AbstractPropertyAccessor;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.MethodInvocationException;
import org.springframework.beans.NotReadablePropertyException;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.beans.NullValueInNestedPathException;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.PropertyEditorRegistrySupport;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.CollectionFactory;
import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.Property;
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;

/**
 * Default {@link BeanWrapper} implementation that should be sufficient for all typical use cases. Caches introspection results for efficiency.
 *
 * <p>
 * Note: Auto-registers default property editors from the {@code org.springframework.beans.propertyeditors} package, which apply in addition to the JDK's standard PropertyEditors. Applications can call the {@link #registerCustomEditor(Class, java.beans.PropertyEditor)} method to
 * register an editor for a particular instance (i.e. they are not shared across the application). See the base class {@link PropertyEditorRegistrySupport} for details.
 *
 * <p>
 * {@code BeanWrapperImpl} 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.
 *
 * <p>
 * <b>NOTE: As of Spring 2.5, this is - for almost all purposes - an internal class.</b> It is just public in order to allow for access from other framework packages. For standard application access purposes, use the {@link PropertyAccessorFactory#forBeanPropertyAccess} factory
 * method instead.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @author Rob Harrop
 * @since 15 April 2001
 * @see #registerCustomEditor
 * @see #setPropertyValues
 * @see #setPropertyValue
 * @see #getPropertyValue
 * @see #getPropertyType
 * @see BeanWrapper
 * @see PropertyEditorRegistrySupport
 */
public class ExtendedBeanWrapperImpl extends AbstractPropertyAccessor implements BeanWrapper {

    /**
     * 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(ExtendedBeanWrapperImpl.class);

    private TypeConverterDelegate typeConverterDelegate;

    private static Class<?> javaUtilOptionalClass = null;

    static {
        try {
            javaUtilOptionalClass = ClassUtils.forName("java.util.Optional",
                    ExtendedBeanWrapperImpl.class.getClassLoader());
        } catch (ClassNotFoundException ex) {
            // Java 8 not available - Optional references simply not supported then.
        }
    }

    /** The wrapped object */
    private Object object;

    private String nestedPath = "";

    private Object rootObject;

    /**
     * The security context used for invoking the property methods
     */
    private AccessControlContext acc;

    /**
     * Cached introspections results for this object, to prevent encountering the cost of JavaBeans introspection every time.
     */
    private CachedIntrospectionResults cachedIntrospectionResults;

    /**
     * Map with cached nested BeanWrappers: nested path -> BeanWrapper instance.
     */
    private Map<String, ExtendedBeanWrapperImpl> nestedBeanWrappers;

    private int autoGrowCollectionLimit = Integer.MAX_VALUE;

    /**
     * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards. Registers default editors.
     * 
     * @see #setWrappedInstance
     */
    public ExtendedBeanWrapperImpl() {
        this(true);
    }

    /**
     * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards.
     * 
     * @param registerDefaultEditors
     *            whether to register default editors (can be suppressed if the BeanWrapper won't need any type conversion)
     * @see #setWrappedInstance
     */
    public ExtendedBeanWrapperImpl(boolean registerDefaultEditors) {
        if (registerDefaultEditors) {
            registerDefaultEditors();
        }
        this.typeConverterDelegate = new TypeConverterDelegate(this);
    }

    /**
     * Create new BeanWrapperImpl for the given object.
     * 
     * @param object
     *            object wrapped by this BeanWrapper
     */
    public ExtendedBeanWrapperImpl(Object object) {
        registerDefaultEditors();
        setWrappedInstance(object);
    }

    /**
     * Create new BeanWrapperImpl, wrapping a new instance of the specified class.
     * 
     * @param clazz
     *            class to instantiate and wrap
     */
    public ExtendedBeanWrapperImpl(Class<?> clazz) {
        registerDefaultEditors();
        setWrappedInstance(BeanUtils.instantiateClass(clazz));
    }

    /**
     * Create new BeanWrapperImpl for the given object, registering a nested path that the object is in.
     * 
     * @param object
     *            object wrapped by this BeanWrapper
     * @param nestedPath
     *            the nested path of the object
     * @param rootObject
     *            the root object at the top of the path
     */
    public ExtendedBeanWrapperImpl(Object object, String nestedPath, Object rootObject) {
        registerDefaultEditors();
        setWrappedInstance(object, nestedPath, rootObject);
    }

    /**
     * Create new BeanWrapperImpl for the given object, registering a nested path that the object is in.
     * 
     * @param object
     *            object wrapped by this BeanWrapper
     * @param nestedPath
     *            the nested path of the object
     * @param superBw
     *            the containing BeanWrapper (must not be {@code null})
     */
    private ExtendedBeanWrapperImpl(Object object, String nestedPath, ExtendedBeanWrapperImpl superBw) {
        setWrappedInstance(object, nestedPath, superBw.getWrappedInstance());
        setExtractOldValueForEditor(superBw.isExtractOldValueForEditor());
        setAutoGrowNestedPaths(superBw.isAutoGrowNestedPaths());
        setAutoGrowCollectionLimit(superBw.getAutoGrowCollectionLimit());
        setConversionService(superBw.getConversionService());
        setSecurityContext(superBw.acc);
    }

    // ---------------------------------------------------------------------
    // Implementation of BeanWrapper interface
    // ---------------------------------------------------------------------

    /**
     * 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.nestedBeanWrappers = null;
        this.typeConverterDelegate = new TypeConverterDelegate(this, this.object);
        setIntrospectionClass(this.object.getClass());
    }

    @Override
    public final Object getWrappedInstance() {
        return this.object;
    }

    @Override
    public final Class<?> getWrappedClass() {
        return (this.object != null ? this.object.getClass() : null);
    }

    /**
     * Return the nested path of the object wrapped by this BeanWrapper.
     */
    public final String getNestedPath() {
        return this.nestedPath;
    }

    /**
     * Return the root object at the top of the path of this BeanWrapper.
     * 
     * @see #getNestedPath
     */
    public final Object getRootInstance() {
        return this.rootObject;
    }

    /**
     * Return the class of the root object at the top of the path of this BeanWrapper.
     * 
     * @see #getNestedPath
     */
    public final Class<?> getRootClass() {
        return (this.rootObject != null ? this.rootObject.getClass() : null);
    }

    /**
     * Specify a limit for array and collection auto-growing.
     * <p>
     * Default is unlimited on a plain BeanWrapper.
     */
    @Override
    public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
        this.autoGrowCollectionLimit = autoGrowCollectionLimit;
    }

    /**
     * Return the limit for array and collection auto-growing.
     */
    @Override
    public int getAutoGrowCollectionLimit() {
        return this.autoGrowCollectionLimit;
    }

    /**
     * Set the security context used during the invocation of the wrapped instance methods. Can be null.
     */
    public void setSecurityContext(AccessControlContext acc) {
        this.acc = acc;
    }

    /**
     * Return the security context used during the invocation of the wrapped instance methods. Can be null.
     */
    public AccessControlContext getSecurityContext() {
        return this.acc;
    }

    /**
     * Set the class to introspect. Needs to be called when the target object changes.
     * 
     * @param clazz
     *            the class to introspect
     */
    protected void setIntrospectionClass(Class<?> clazz) {
        if (this.cachedIntrospectionResults != null
                && !clazz.equals(this.cachedIntrospectionResults.getBeanClass())) {
            this.cachedIntrospectionResults = null;
        }
    }

    /**
     * Obtain a lazily initializted CachedIntrospectionResults instance for the wrapped object.
     */
    private CachedIntrospectionResults getCachedIntrospectionResults() {
        Assert.state(this.object != null, "BeanWrapper does not hold a bean instance");
        if (this.cachedIntrospectionResults == null) {
            this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(getWrappedClass());
        }
        return this.cachedIntrospectionResults;
    }

    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
        return getCachedIntrospectionResults().getPropertyDescriptors();
    }

    @Override
    public PropertyDescriptor getPropertyDescriptor(String propertyName) throws BeansException {
        PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
        if (pd == null) {
            throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
                    "No property '" + propertyName + "' found");
        }
        return pd;
    }

    /**
     * Internal version of {@link #getPropertyDescriptor}: Returns {@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 PropertyDescriptor getPropertyDescriptorInternal(String propertyName) throws BeansException {
        Assert.notNull(propertyName, "Property name must not be null");
        ExtendedBeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
        return nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(getFinalPath(nestedBw, propertyName));
    }

    @Override
    public Class<?> getPropertyType(String propertyName) throws BeansException {
        try {
            PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
            if (pd != null) {
                return pd.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 {
            ExtendedBeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
            String finalPath = getFinalPath(nestedBw, propertyName);
            PropertyTokenHolder tokens = getPropertyNameTokens(finalPath);
            PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults()
                    .getPropertyDescriptor(tokens.actualName);
            if (pd != null) {
                if (tokens.keys != null) {
                    if (pd.getReadMethod() != null || pd.getWriteMethod() != null) {
                        return TypeDescriptor.nested(property(pd), tokens.keys.length);
                    }
                } else {
                    if (pd.getReadMethod() != null || pd.getWriteMethod() != null) {
                        return new TypeDescriptor(property(pd));
                    }
                }
            }
        } catch (InvalidPropertyException ex) {
            // Consider as not determinable.
        }
        return null;
    }

    @Override
    public boolean isReadableProperty(String propertyName) {
        try {
            PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
            if (pd != null) {
                if (pd.getReadMethod() != null) {
                    return true;
                }
            } 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 {
            PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
            if (pd != null) {
                if (pd.getWriteMethod() != null) {
                    return true;
                }
            } 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);
        }
    }

    /**
     * Convert the given value for the specified property to the latter's type.
     * <p>
     * This method is only intended for optimizations in a BeanFactory. Use the {@code convertIfNecessary} methods for programmatic conversion.
     * 
     * @param value
     *            the value to convert
     * @param propertyName
     *            the target property (note that nested or indexed properties are not supported here)
     * @return the new value, possibly the result of type conversion
     * @throws TypeMismatchException
     *             if type conversion failed
     */
    public Object convertForProperty(Object value, String propertyName) throws TypeMismatchException {
        CachedIntrospectionResults cachedIntrospectionResults = getCachedIntrospectionResults();
        PropertyDescriptor pd = cachedIntrospectionResults.getPropertyDescriptor(propertyName);
        if (pd == null) {
            throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
                    "No property '" + propertyName + "' found");
        }
        TypeDescriptor td = cachedIntrospectionResults.getTypeDescriptor(pd);
        if (td == null) {
            td = cachedIntrospectionResults.addTypeDescriptor(pd, new TypeDescriptor(property(pd)));
        }
        return convertForProperty(propertyName, null, value, td);
    }

    private Object convertForProperty(String propertyName, Object oldValue, Object newValue, TypeDescriptor td)
            throws TypeMismatchException {

        return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);
    }

    private Property property(PropertyDescriptor pd) {
        GenericTypeAwarePropertyDescriptor typeAware = (GenericTypeAwarePropertyDescriptor) pd;
        return new Property(typeAware.getBeanClass(), typeAware.getReadMethod(), typeAware.getWriteMethod(),
                typeAware.getName());
    }

    // ---------------------------------------------------------------------
    // Implementation methods
    // ---------------------------------------------------------------------

    /**
     * Get the last component of the path. Also works if not nested.
     * 
     * @param bw
     *            BeanWrapper 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(BeanWrapper bw, String nestedPath) {
        if (bw == this) {
            return nestedPath;
        }
        return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1);
    }

    /**
     * Recursively navigate to return a BeanWrapper for the nested property path.
     * 
     * @param propertyPath
     *            property property path, which may be nested
     * @return a BeanWrapper for the target bean
     */
    protected ExtendedBeanWrapperImpl getBeanWrapperForPropertyPath(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);
            ExtendedBeanWrapperImpl nestedBw = getNestedBeanWrapper(nestedProperty);
            return nestedBw.getBeanWrapperForPropertyPath(nestedPath);
        } else {
            return this;
        }
    }

    /**
     * Retrieve a BeanWrapper for the given nested property. Create a new one if not found in the cache.
     * <p>
     * Note: Caching nested BeanWrappers is necessary now, to keep registered custom editors for nested properties.
     * 
     * @param nestedProperty
     *            property to create the BeanWrapper for
     * @return the BeanWrapper instance, either cached or newly created
     */
    private ExtendedBeanWrapperImpl getNestedBeanWrapper(String nestedProperty) {
        if (this.nestedBeanWrappers == null) {
            this.nestedBeanWrappers = new HashMap<String, ExtendedBeanWrapperImpl>();
        }
        // 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-BeanWrapper, create new one if not found.
        ExtendedBeanWrapperImpl nestedBw = this.nestedBeanWrappers.get(canonicalName);
        if (nestedBw == null || nestedBw.getWrappedInstance() != (value.getClass().equals(javaUtilOptionalClass)
                ? OptionalUnwrapper.unwrap(value)
                : value)) {
            if (logger.isTraceEnabled()) {
                logger.trace("Creating new nested BeanWrapper for property '" + canonicalName + "'");
            }
            nestedBw = newNestedBeanWrapper(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR);
            // Inherit all type-specific PropertyEditors.
            copyDefaultEditorsTo(nestedBw);
            copyCustomEditorsTo(nestedBw, canonicalName);
            this.nestedBeanWrappers.put(canonicalName, nestedBw);
        } else {
            if (logger.isTraceEnabled()) {
                logger.trace("Using cached nested BeanWrapper for property '" + canonicalName + "'");
            }
        }
        return nestedBw;
    }

    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();
                // #TO#DO# - 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 type.newInstance();
            }
        } catch (Exception ex) {
            // #TO#DO#: 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);
        }
    }

    /**
     * Create a new nested BeanWrapper instance.
     * <p>
     * Default implementation creates a BeanWrapperImpl instance. Can be overridden in subclasses to create a BeanWrapperImpl subclass.
     * 
     * @param object
     *            object wrapped by this BeanWrapper
     * @param nestedPath
     *            the nested path of the object
     * @return the nested BeanWrapper instance
     */
    protected ExtendedBeanWrapperImpl newNestedBeanWrapper(Object object, String nestedPath) {
        return new ExtendedBeanWrapperImpl(object, nestedPath, this);
    }

    /**
     * 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<String> keys = new ArrayList<String>(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;
    }

    // ---------------------------------------------------------------------
    // Implementation of PropertyAccessor interface
    // ---------------------------------------------------------------------

    @Override
    public Object getPropertyValue(String propertyName) throws BeansException {
        ExtendedBeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
        PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName));
        return nestedBw.getPropertyValue(tokens);
    }

    @SuppressWarnings("unchecked")
    private Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException {
        String propertyName = tokens.canonicalName;
        String actualName = tokens.actualName;
        PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
        if (pd == null || pd.getReadMethod() == null) {
            throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName);
        }
        final Method readMethod = pd.getReadMethod();
        try {
            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers()) && !readMethod.isAccessible()) {
                if (System.getSecurityManager() != null) {
                    AccessController.doPrivileged(new PrivilegedAction<Object>() {
                        @Override
                        public Object run() {
                            readMethod.setAccessible(true);
                            return null;
                        }
                    });
                } else {
                    readMethod.setAccessible(true);
                }
            }

            Object value;
            if (System.getSecurityManager() != null) {
                try {
                    value = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                        @Override
                        public Object run() throws Exception {
                            return readMethod.invoke(object, (Object[]) null);
                        }
                    }, acc);
                } catch (PrivilegedActionException pae) {
                    throw pae.getException();
                }
            } else {
                value = readMethod.invoke(object, (Object[]) null);
            }

            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<Object> list = (List<Object>) value;
                        growCollectionIfNecessary(list, index, indexedPropertyName, pd, 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 = GenericCollectionTypeResolver.getMapKeyReturnType(pd.getReadMethod(),
                                i + 1);
                        // 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);
        }
    }

    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));
            }
            // #TO#DO# 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<Object> collection, int index, String name,
            PropertyDescriptor pd, int nestingLevel) {

        if (!isAutoGrowNestedPaths()) {
            return;
        }
        int size = collection.size();
        if (index >= size && index < this.autoGrowCollectionLimit) {
            Class<?> elementType = GenericCollectionTypeResolver.getCollectionReturnType(pd.getReadMethod(),
                    nestingLevel);
            if (elementType != null) {
                for (int i = collection.size(); i < index + 1; i++) {
                    collection.add(newValue(elementType, null, name));
                }
            }
        }
    }

    @Override
    public void setPropertyValue(String propertyName, Object value) throws BeansException {
        ExtendedBeanWrapperImpl nestedBw;
        try {
            nestedBw = getBeanWrapperForPropertyPath(propertyName);
        } catch (NotReadablePropertyException ex) {
            throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
                    "Nested property in path '" + propertyName + "' does not exist", ex);
        }
        PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName));
        nestedBw.setPropertyValue(tokens, new PropertyValue(propertyName, value));
    }

    @Override
    public void setPropertyValue(PropertyValue pv2) throws BeansException {
        net.yasion.common.core.bean.wrapper.PropertyValue pv = new net.yasion.common.core.bean.wrapper.PropertyValue(
                "", null);
        AfxBeanUtils.copySamePropertyValue(pv2, pv);
        PropertyTokenHolder tokens = (PropertyTokenHolder) pv.getResolvedTokens();
        if (tokens == null) {
            String propertyName = pv.getName();
            ExtendedBeanWrapperImpl nestedBw;
            try {
                nestedBw = getBeanWrapperForPropertyPath(propertyName);
            } catch (NotReadablePropertyException ex) {
                throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
                        "Nested property in path '" + propertyName + "' does not exist", ex);
            }
            tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName));
            if (nestedBw == this) {
                pv.getOriginalPropertyValue().setResolvedTokens(tokens);
            }
            AfxBeanUtils.copySamePropertyValue(pv, pv2);
            nestedBw.setPropertyValue(tokens, pv2);
        } else {
            AfxBeanUtils.copySamePropertyValue(pv, pv2);
            setPropertyValue(tokens, pv2);
        }
    }

    @SuppressWarnings("unchecked")
    private void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv2) throws BeansException {
        net.yasion.common.core.bean.wrapper.PropertyValue pv = new net.yasion.common.core.bean.wrapper.PropertyValue(
                "", null);
        AfxBeanUtils.copySamePropertyValue(pv2, pv);
        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()) {
                    // #TO#DO#: 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()) {
                PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(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,
                            TypeDescriptor.nested(property(pd), tokens.keys.length));
                    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) {
                PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
                Class<?> requiredType = GenericCollectionTypeResolver.getCollectionReturnType(pd.getReadMethod(),
                        tokens.keys.length);
                List<Object> list = (List<Object>) 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,
                        TypeDescriptor.nested(property(pd), 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) {
                PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
                Class<?> mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType(pd.getReadMethod(),
                        tokens.keys.length);
                Class<?> mapValueType = GenericCollectionTypeResolver.getMapValueReturnType(pd.getReadMethod(),
                        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, 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,
                        TypeDescriptor.nested(property(pd), 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 [" + pv.getValue()
                                + "]");
            }
        }

        else {
            PropertyDescriptor pd = pv.getResolvedDescriptor();
            if (pd == null || !pd.getWriteMethod().getDeclaringClass().isInstance(this.object)) {
                pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
                if (pd == null || pd.getWriteMethod() == null) {
                    if (pv.isOptional()) {
                        logger.debug("Ignoring optional value for property '" + actualName
                                + "' - property not found on bean class [" + getRootClass().getName() + "]");
                        return;
                    } else {
                        PropertyMatches matches = PropertyMatches.forProperty(propertyName, getRootClass());
                        throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
                                matches.buildErrorMessage(), matches.getPossibleMatches());
                    }
                }
                pv.getOriginalPropertyValue().setResolvedDescriptor(pd);
            }

            Object oldValue = null;
            try {
                Object originalValue = pv.getValue();
                Object valueToApply = originalValue;
                if (!Boolean.FALSE.equals(pv.getConversionNecessary())) {
                    if (pv.isConverted()) {
                        valueToApply = pv.getConvertedValue();
                    } else {
                        if (isExtractOldValueForEditor() && pd.getReadMethod() != null) {
                            final Method readMethod = pd.getReadMethod();
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())
                                    && !readMethod.isAccessible()) {
                                if (System.getSecurityManager() != null) {
                                    AccessController.doPrivileged(new PrivilegedAction<Object>() {
                                        @Override
                                        public Object run() {
                                            readMethod.setAccessible(true);
                                            return null;
                                        }
                                    });
                                } else {
                                    readMethod.setAccessible(true);
                                }
                            }
                            try {
                                if (System.getSecurityManager() != null) {
                                    oldValue = AccessController
                                            .doPrivileged(new PrivilegedExceptionAction<Object>() {
                                                @Override
                                                public Object run() throws Exception {
                                                    return readMethod.invoke(object);
                                                }
                                            }, acc);
                                } else {
                                    oldValue = readMethod.invoke(object);
                                }
                            } 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,
                                new TypeDescriptor(property(pd)));
                    }
                    pv.getOriginalPropertyValue().setConversionNecessary(valueToApply != originalValue);
                }
                final Method writeMethod = (pd instanceof GenericTypeAwarePropertyDescriptor
                        ? ((GenericTypeAwarePropertyDescriptor) pd).getWriteMethodForActualAccess()
                        : pd.getWriteMethod());
                if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())
                        && !writeMethod.isAccessible()) {
                    if (System.getSecurityManager() != null) {
                        AccessController.doPrivileged(new PrivilegedAction<Object>() {
                            @Override
                            public Object run() {
                                writeMethod.setAccessible(true);
                                return null;
                            }
                        });
                    } else {
                        writeMethod.setAccessible(true);
                    }
                }
                final Object value = valueToApply;
                if (System.getSecurityManager() != null) {
                    try {
                        AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                            @Override
                            public Object run() throws Exception {
                                writeMethod.invoke(object, value);
                                return null;
                            }
                        }, acc);
                    } catch (PrivilegedActionException ex) {
                        throw ex.getException();
                    }
                } else {
                    writeMethod.invoke(this.object, value);
                }
            } 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, pd.getPropertyType(),
                            ex.getTargetException());
                } else {
                    throw new MethodInvocationException(propertyChangeEvent, ex.getTargetException());
                }
            } catch (Exception ex) {
                PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName,
                        oldValue, pv.getValue());
                throw new MethodInvocationException(pce, ex);
            }
        }
    }

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

    private 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();
        }
    }

}