/* * Copyright 2002-2008 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.beans; import java.beans.PropertyChangeEvent; import java.beans.PropertyDescriptor; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.JdkVersion; import org.springframework.core.MethodParameter; import org.springframework.util.Assert; 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</code> 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</code> 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</code>, or against a * comma-delimited String via <code>setAsText</code>, 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 BeanWrapperImpl 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(BeanWrapperImpl.class); /** The wrapped object */ private Object object; private String nestedPath = ""; private Object rootObject; private TypeConverterDelegate typeConverterDelegate; /** * 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 nestedBeanWrappers; /** * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards. * Registers default editors. * @see #setWrappedInstance */ public BeanWrapperImpl() { 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 BeanWrapperImpl(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 BeanWrapperImpl(Object object) { registerDefaultEditors(); setWrappedInstance(object); } /** * Create new BeanWrapperImpl, wrapping a new instance of the specified class. * @param clazz class to instantiate and wrap */ public BeanWrapperImpl(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 BeanWrapperImpl(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</code>) */ private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl superBw) { setWrappedInstance(object, nestedPath, superBw.getWrappedInstance()); setExtractOldValueForEditor(superBw.isExtractOldValueForEditor()); } //--------------------------------------------------------------------- // 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"); this.object = object; this.nestedPath = (nestedPath != null ? nestedPath : ""); this.rootObject = (!"".equals(this.nestedPath) ? rootObject : object); this.nestedBeanWrappers = null; this.typeConverterDelegate = new TypeConverterDelegate(this, object); setIntrospectionClass(object.getClass()); } public final Object getWrappedInstance() { return this.object; } public final Class getWrappedClass() { return (this.object != null ? this.object.getClass() : null); } /** * Return the nested path of the object wrapped by this 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); } /** * 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; } public PropertyDescriptor[] getPropertyDescriptors() { return getCachedIntrospectionResults().getBeanInfo().getPropertyDescriptors(); } 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</code> 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</code> 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"); BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName); return nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(getFinalPath(nestedBw, propertyName)); } 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; } 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; } 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; } /** * @deprecated in favor of <code>convertIfNecessary</code> * @see #convertIfNecessary(Object, Class) */ public Object doTypeConversionIfNecessary(Object value, Class requiredType) throws TypeMismatchException { return convertIfNecessary(value, requiredType, null); } public Object convertIfNecessary( Object value, Class requiredType, MethodParameter methodParam) throws TypeMismatchException { try { return this.typeConverterDelegate.convertIfNecessary(value, requiredType, methodParam); } catch (IllegalArgumentException ex) { throw new TypeMismatchException(value, 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</code> 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 { PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); if (pd == null) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "No property '" + propertyName + "' found"); } try { return this.typeConverterDelegate.convertIfNecessary(null, value, pd); } catch (IllegalArgumentException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, null, value); throw new TypeMismatchException(pce, pd.getPropertyType(), ex); } } //--------------------------------------------------------------------- // 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 BeanWrapperImpl 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); BeanWrapperImpl 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 BeanWrapperImpl getNestedBeanWrapper(String nestedProperty) { if (this.nestedBeanWrappers == null) { this.nestedBeanWrappers = new HashMap(); } // Get value of bean property. PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); String canonicalName = tokens.canonicalName; Object propertyValue = getPropertyValue(tokens); if (propertyValue == null) { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName); } // Lookup cached sub-BeanWrapper, create new one if not found. BeanWrapperImpl nestedBw = (BeanWrapperImpl) this.nestedBeanWrappers.get(canonicalName); if (nestedBw == null || nestedBw.getWrappedInstance() != propertyValue) { if (logger.isTraceEnabled()) { logger.trace("Creating new nested BeanWrapper for property '" + canonicalName + "'"); } nestedBw = newNestedBeanWrapper(propertyValue, 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; } /** * 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 BeanWrapperImpl newNestedBeanWrapper(Object object, String nestedPath) { return new BeanWrapperImpl(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 keys = new ArrayList(2); int searchIndex = 0; while (searchIndex != -1) { int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex); searchIndex = -1; if (keyStart != -1) { int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length()); if (keyEnd != -1) { if (actualName == null) { actualName = propertyName.substring(0, keyStart); } String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { key = key.substring(1, key.length() - 1); } keys.add(key); searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); } } } tokens.actualName = (actualName != null ? actualName : propertyName); tokens.canonicalName = tokens.actualName; if (!keys.isEmpty()) { tokens.canonicalName += PROPERTY_KEY_PREFIX + StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) + PROPERTY_KEY_SUFFIX; tokens.keys = StringUtils.toStringArray(keys); } return tokens; } //--------------------------------------------------------------------- // Implementation of PropertyAccessor interface //--------------------------------------------------------------------- public Object getPropertyValue(String propertyName) throws BeansException { BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName); PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName)); return nestedBw.getPropertyValue(tokens); } 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); } Method readMethod = pd.getReadMethod(); try { if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { readMethod.setAccessible(true); } Object value = readMethod.invoke(this.object, (Object[]) null); if (tokens.keys != null) { // 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()) { value = Array.get(value, Integer.parseInt(key)); } else if (value instanceof List) { List list = (List) value; value = list.get(Integer.parseInt(key)); } else if (value instanceof Set) { // Apply index to Iterator in case of a Set. Set set = (Set) value; int index = Integer.parseInt(key); if (index < 0 || index >= set.size()) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Cannot get element with index " + index + " from Set of size " + set.size() + ", accessed using property path '" + propertyName + "'"); } Iterator it = set.iterator(); for (int j = 0; it.hasNext(); j++) { Object elem = it.next(); if (j == index) { value = elem; break; } } } else if (value instanceof Map) { Map map = (Map) value; Class mapKeyType = null; if (JdkVersion.isAtLeastJava15()) { 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. Object convertedMapKey = this.typeConverterDelegate.convertIfNecessary(key, mapKeyType); // Pass full property name and old value in here, since we want full // conversion ability for map values. 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 + "]"); } } } return value; } catch (InvocationTargetException ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Getter for property '" + actualName + "' threw exception", ex); } catch (IllegalAccessException ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Illegal attempt to get property '" + actualName + "' threw exception", ex); } 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); } } public void setPropertyValue(String propertyName, Object value) throws BeansException { BeanWrapperImpl nestedBw = null; 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)); } public void setPropertyValue(PropertyValue pv) throws BeansException { PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens; if (tokens == null) { String propertyName = pv.getName(); BeanWrapperImpl nestedBw = null; 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().resolvedTokens = tokens; } nestedBw.setPropertyValue(tokens, pv); } else { setPropertyValue(tokens, pv); } } private void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { String propertyName = tokens.canonicalName; String actualName = tokens.actualName; if (tokens.keys != null) { // Apply indexes and map keys: fetch value for all keys but the last one. PropertyTokenHolder getterTokens = new PropertyTokenHolder(); getterTokens.canonicalName = tokens.canonicalName; getterTokens.actualName = tokens.actualName; getterTokens.keys = new String[tokens.keys.length - 1]; System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1); Object propValue = null; 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) { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, "Cannot access indexed value in property referenced " + "in indexed property path '" + propertyName + "': returned null"); } else if (propValue.getClass().isArray()) { Class requiredType = propValue.getClass().getComponentType(); int arrayIndex = Integer.parseInt(key); Object oldValue = null; try { if (isExtractOldValueForEditor()) { oldValue = Array.get(propValue, arrayIndex); } Object convertedValue = this.typeConverterDelegate.convertIfNecessary( propertyName, oldValue, pv.getValue(), requiredType); Array.set(propValue, Integer.parseInt(key), convertedValue); } catch (IllegalArgumentException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); throw new TypeMismatchException(pce, requiredType, ex); } 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 = null; if (JdkVersion.isAtLeastJava15()) { requiredType = GenericCollectionTypeResolver.getCollectionReturnType( pd.getReadMethod(), tokens.keys.length); } List list = (List) propValue; int index = Integer.parseInt(key); Object oldValue = null; if (isExtractOldValueForEditor() && index < list.size()) { oldValue = list.get(index); } try { Object convertedValue = this.typeConverterDelegate.convertIfNecessary( propertyName, oldValue, pv.getValue(), requiredType); if (index < list.size()) { list.set(index, convertedValue); } else if (index >= list.size()) { for (int i = list.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 " + list.size() + ", accessed using property path '" + propertyName + "': List does not support filling up gaps with null elements"); } } list.add(convertedValue); } } catch (IllegalArgumentException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); throw new TypeMismatchException(pce, requiredType, ex); } } else if (propValue instanceof Map) { PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName); Class mapKeyType = null; Class mapValueType = null; if (JdkVersion.isAtLeastJava15()) { mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType( pd.getReadMethod(), tokens.keys.length); mapValueType = GenericCollectionTypeResolver.getMapValueReturnType( pd.getReadMethod(), tokens.keys.length); } Map map = (Map) propValue; Object convertedMapKey = null; Object convertedMapValue = null; try { // IMPORTANT: Do not pass full property name in here - property editors // must not kick in for map keys but rather only for map values. convertedMapKey = this.typeConverterDelegate.convertIfNecessary(key, mapKeyType); } catch (IllegalArgumentException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, null, pv.getValue()); throw new TypeMismatchException(pce, mapKeyType, ex); } Object oldValue = null; if (isExtractOldValueForEditor()) { oldValue = map.get(convertedMapKey); } try { // Pass full property name and old value in here, since we want full // conversion ability for map values. convertedMapValue = this.typeConverterDelegate.convertIfNecessary( propertyName, oldValue, pv.getValue(), mapValueType, null, new MethodParameter(pd.getReadMethod(), -1, tokens.keys.length + 1)); } catch (IllegalArgumentException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); throw new TypeMismatchException(pce, mapValueType, ex); } 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.resolvedDescriptor; if (pd == null || !pd.getWriteMethod().getDeclaringClass().isInstance(this.object)) { pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName); if (pd == null || pd.getWriteMethod() == null) { PropertyMatches matches = PropertyMatches.forProperty(propertyName, getRootClass()); throw new NotWritablePropertyException( getRootClass(), this.nestedPath + propertyName, matches.buildErrorMessage(), matches.getPossibleMatches()); } pv.getOriginalPropertyValue().resolvedDescriptor = pd; } 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() && pd.getReadMethod() != null) { Method readMethod = pd.getReadMethod(); if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { readMethod.setAccessible(true); } try { oldValue = readMethod.invoke(this.object, new Object[0]); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Could not read previous value of property '" + this.nestedPath + propertyName + "'", ex); } } } valueToApply = this.typeConverterDelegate.convertIfNecessary(oldValue, originalValue, pd); } pv.getOriginalPropertyValue().conversionNecessary = Boolean.valueOf(valueToApply != originalValue); } Method writeMethod = pd.getWriteMethod(); if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) { writeMethod.setAccessible(true); } writeMethod.invoke(this.object, new Object[] {valueToApply}); } 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 (IllegalArgumentException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); throw new TypeMismatchException(pce, pd.getPropertyType(), ex); } catch (IllegalAccessException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue()); throw new MethodInvocationException(pce, ex); } } } public String toString() { StringBuffer sb = new StringBuffer(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(); } //--------------------------------------------------------------------- // Inner class for internal use //--------------------------------------------------------------------- private static class PropertyTokenHolder { public String canonicalName; public String actualName; public String[] keys; } }