All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.beans.BeanWrapperImpl Maven / Gradle / Ivy

There is a newer version: 6.1.11
Show newest version
/*
 * Copyright 2002-2005 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.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.propertyeditors.ByteArrayPropertyEditor;
import org.springframework.beans.propertyeditors.CharacterEditor;
import org.springframework.beans.propertyeditors.ClassEditor;
import org.springframework.beans.propertyeditors.CustomBooleanEditor;
import org.springframework.beans.propertyeditors.CustomCollectionEditor;
import org.springframework.beans.propertyeditors.CustomNumberEditor;
import org.springframework.beans.propertyeditors.FileEditor;
import org.springframework.beans.propertyeditors.InputStreamEditor;
import org.springframework.beans.propertyeditors.LocaleEditor;
import org.springframework.beans.propertyeditors.PropertiesEditor;
import org.springframework.beans.propertyeditors.StringArrayPropertyEditor;
import org.springframework.beans.propertyeditors.URLEditor;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceArrayPropertyEditor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Default implementation of the BeanWrapper interface that should be sufficient
 * for all typical use cases. Caches introspection results for efficiency.
 *
 * 

Note: This class never tries to load a class by name, as this can pose * class loading problems in J2EE applications with multiple deployment modules. * The caller is responsible for loading a target class. * *

Note: Auto-registers default property editors from the * org.springframework.beans.propertyeditors package, which apply * in addition to the JDK's standard PropertyEditors. Applications can call * BeanWrapper's registerCustomEditor method to register an editor * for the particular instance (i.e. they're not shared across the application). * *

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 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 Rod Johnson * @author Juergen Hoeller * @author Rob Harrop * @see #registerCustomEditor * @see java.beans.PropertyEditorManager * @see java.beans.PropertyEditorSupport#setAsText * @see java.beans.PropertyEditorSupport#setValue * @see org.springframework.beans.propertyeditors.ByteArrayPropertyEditor * @see org.springframework.beans.propertyeditors.ClassEditor * @see org.springframework.beans.propertyeditors.CharacterEditor * @see org.springframework.beans.propertyeditors.CustomBooleanEditor * @see org.springframework.beans.propertyeditors.CustomNumberEditor * @see org.springframework.beans.propertyeditors.CustomCollectionEditor * @see org.springframework.beans.propertyeditors.FileEditor * @see org.springframework.beans.propertyeditors.InputStreamEditor * @see org.springframework.jndi.JndiTemplateEditor * @see org.springframework.beans.propertyeditors.LocaleEditor * @see org.springframework.beans.propertyeditors.PropertiesEditor * @see org.springframework.beans.PropertyValuesEditor * @see org.springframework.core.io.support.ResourceArrayPropertyEditor * @see org.springframework.core.io.ResourceEditor * @see org.springframework.beans.propertyeditors.StringArrayPropertyEditor * @see org.springframework.transaction.interceptor.TransactionAttributeEditor * @see org.springframework.transaction.interceptor.TransactionAttributeSourceEditor * @see org.springframework.beans.propertyeditors.URLEditor * @since 15 April 2001 */ public class BeanWrapperImpl 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); //--------------------------------------------------------------------- // Instance data //--------------------------------------------------------------------- /** * The wrapped object */ private Object object; /** * The nested path of the object */ private String nestedPath = ""; private Object rootObject; /** * Registry for default PropertyEditors */ private final Map defaultEditors; /** * Map with custom PropertyEditor instances */ private Map customEditors; /** * Cached introspections results for this object, to prevent encountering * the cost of JavaBeans introspection every time. */ private CachedIntrospectionResults cachedIntrospectionResults; /* Map with cached nested BeanWrappers */ private Map nestedBeanWrappers; //--------------------------------------------------------------------- // Constructors //--------------------------------------------------------------------- /** * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards. * @see #setWrappedInstance */ public BeanWrapperImpl() { this.defaultEditors = new HashMap(32); registerDefaultEditors(); } /** * Create new BeanWrapperImpl for the given object. * @param object object wrapped by this BeanWrapper */ public BeanWrapperImpl(Object object) { this(); setWrappedInstance(object); } /** * Create new BeanWrapperImpl, wrapping a new instance of the specified class. * @param clazz class to instantiate and wrap */ public BeanWrapperImpl(Class clazz) { this(); 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) { this(); 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 null) */ private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl superBw) { this.defaultEditors = superBw.defaultEditors; setWrappedInstance(object, nestedPath, superBw.getWrappedInstance()); } /** * Register default editors in this class, for restricted environments. * We're not using the JRE's PropertyEditorManager to avoid potential * SecurityExceptions when running in a SecurityManager. *

Registers a CustomNumberEditor for all primitive number types, * their corresponding wrapper types, BigInteger and BigDecimal. */ private void registerDefaultEditors() { // Simple editors, without parameterization capabilities. // The JDK does not contain a default editor for any of these target types. this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor()); this.defaultEditors.put(Class.class, new ClassEditor()); this.defaultEditors.put(File.class, new FileEditor()); this.defaultEditors.put(InputStream.class, new InputStreamEditor()); this.defaultEditors.put(Locale.class, new LocaleEditor()); this.defaultEditors.put(Properties.class, new PropertiesEditor()); this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor()); this.defaultEditors.put(String[].class, new StringArrayPropertyEditor()); this.defaultEditors.put(URL.class, new URLEditor()); // Default instances of collection editors. // Can be overridden by registering custom instances of those as custom editors. this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class)); this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class)); this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class)); this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class)); // Default instances of character and boolean editors. // Can be overridden by registering custom instances of those as custom editors. PropertyEditor characterEditor = new CharacterEditor(false); PropertyEditor booleanEditor = new CustomBooleanEditor(false); // The JDK does not contain a default editor for char! this.defaultEditors.put(char.class, characterEditor); this.defaultEditors.put(Character.class, characterEditor); // Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor. this.defaultEditors.put(boolean.class, booleanEditor); this.defaultEditors.put(Boolean.class, booleanEditor); // The JDK does not contain default editors for number wrapper types! // Override JDK primitive number editors with our own CustomNumberEditor. PropertyEditor byteEditor = new CustomNumberEditor(Byte.class, false); PropertyEditor shortEditor = new CustomNumberEditor(Short.class, false); PropertyEditor integerEditor = new CustomNumberEditor(Integer.class, false); PropertyEditor longEditor = new CustomNumberEditor(Long.class, false); PropertyEditor floatEditor = new CustomNumberEditor(Float.class, false); PropertyEditor doubleEditor = new CustomNumberEditor(Double.class, false); this.defaultEditors.put(byte.class, byteEditor); this.defaultEditors.put(Byte.class, byteEditor); this.defaultEditors.put(short.class, shortEditor); this.defaultEditors.put(Short.class, shortEditor); this.defaultEditors.put(int.class, integerEditor); this.defaultEditors.put(Integer.class, integerEditor); this.defaultEditors.put(long.class, longEditor); this.defaultEditors.put(Long.class, longEditor); this.defaultEditors.put(float.class, floatEditor); this.defaultEditors.put(Float.class, floatEditor); this.defaultEditors.put(double.class, doubleEditor); this.defaultEditors.put(Double.class, doubleEditor); this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, false)); this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, false)); } //--------------------------------------------------------------------- // Implementation of BeanWrapper //--------------------------------------------------------------------- /** * 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 new target */ 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 new target * @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) { if (object == null) { throw new IllegalArgumentException("Cannot set BeanWrapperImpl target to a null object"); } this.object = object; this.nestedPath = (nestedPath != null ? nestedPath : ""); this.rootObject = (!"".equals(this.nestedPath) ? rootObject : object); this.nestedBeanWrappers = null; setIntrospectionClass(object.getClass()); } public Object getWrappedInstance() { return this.object; } public Class getWrappedClass() { return this.object.getClass(); } /** * Return the nested path of the object wrapped by this BeanWrapper. */ public String getNestedPath() { return this.nestedPath; } /** * Return the root object at the top of the path of this BeanWrapper. * @see #getNestedPath */ public Object getRootInstance() { return this.rootObject; } /** * Return the class of the root object at the top of the path of this BeanWrapper. * @see #getNestedPath */ public 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 || !this.cachedIntrospectionResults.getBeanClass().equals(clazz)) { this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(clazz); } } public void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor) { registerCustomEditor(requiredType, null, propertyEditor); } public void registerCustomEditor(Class requiredType, String propertyPath, PropertyEditor propertyEditor) { if (requiredType == null && propertyPath == null) { throw new IllegalArgumentException("Either requiredType or propertyPath is required"); } if (this.customEditors == null) { this.customEditors = new HashMap(); } if (propertyPath != null) { this.customEditors.put(propertyPath, new CustomEditorHolder(propertyEditor, requiredType)); } else { this.customEditors.put(requiredType, propertyEditor); } } public PropertyEditor findCustomEditor(Class requiredType, String propertyPath) { if (this.customEditors == null) { return null; } if (propertyPath != null) { // check property-specific editor first PropertyEditor editor = getCustomEditor(propertyPath, requiredType); if (editor == null) { List strippedPaths = new LinkedList(); addStrippedPropertyPaths(strippedPaths, "", propertyPath); for (Iterator it = strippedPaths.iterator(); it.hasNext() && editor == null;) { String strippedPath = (String) it.next(); editor = getCustomEditor(strippedPath, requiredType); } } if (editor != null) { return editor; } else if (requiredType == null) { requiredType = getPropertyType(propertyPath); } } // no property-specific editor -> check type-specific editor return getCustomEditor(requiredType); } /** * Get custom editor that has been registered for the given property. * @return the custom editor, or null if none specific for this property */ private PropertyEditor getCustomEditor(String propertyName, Class requiredType) { CustomEditorHolder holder = (CustomEditorHolder) this.customEditors.get(propertyName); return (holder != null ? holder.getPropertyEditor(requiredType) : null); } /** * Get custom editor for the given type. If no direct match found, * try custom editor for superclass (which will in any case be able * to render a value as String via getAsText). * @return the custom editor, or null if none found for this type * @see java.beans.PropertyEditor#getAsText */ private PropertyEditor getCustomEditor(Class requiredType) { if (requiredType != null) { PropertyEditor editor = (PropertyEditor) this.customEditors.get(requiredType); if (editor == null) { for (Iterator it = this.customEditors.keySet().iterator(); it.hasNext();) { Object key = it.next(); if (key instanceof Class && ((Class) key).isAssignableFrom(requiredType)) { editor = (PropertyEditor) this.customEditors.get(key); } } } return editor; } return null; } /** * Add property paths with all variations of stripped keys and/or indexes. * Invokes itself recursively with nested paths * @param strippedPaths the result list to add to * @param nestedPath the current nested path * @param propertyPath the property path to check for keys/indexes to strip */ private void addStrippedPropertyPaths(List strippedPaths, String nestedPath, String propertyPath) { int startIndex = propertyPath.indexOf(PROPERTY_KEY_PREFIX_CHAR); if (startIndex != -1) { int endIndex = propertyPath.indexOf(PROPERTY_KEY_SUFFIX_CHAR); if (endIndex != -1) { String prefix = propertyPath.substring(0, startIndex); String key = propertyPath.substring(startIndex, endIndex + 1); String suffix = propertyPath.substring(endIndex + 1, propertyPath.length()); // strip the first key strippedPaths.add(nestedPath + prefix + suffix); // search for further keys to strip, with the first key stripped addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix); // search for further keys to strip, with the first key not stripped addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix); } } } /** * Determine the first (or last) nested property separator in the * given property path, ignoring dots in keys (like "map[my.key]"). * @param propertyPath the property path to check * @param last whether to return the last separator rather than the first * @return the index of the nested property separator, or -1 if none */ private int getNestedPropertySeparatorIndex(String propertyPath, boolean last) { boolean inKey = false; int i = (last ? propertyPath.length() - 1 : 0); while ((last && i >= 0) || i < propertyPath.length()) { switch (propertyPath.charAt(i)) { case PROPERTY_KEY_PREFIX_CHAR: case PROPERTY_KEY_SUFFIX_CHAR: inKey = !inKey; break; case NESTED_PROPERTY_SEPARATOR_CHAR: if (!inKey) { return i; } } if (last) i--; else i++; } return -1; } /** * 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(getNestedPropertySeparatorIndex(nestedPath, true) + 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) throws BeansException { int pos = getNestedPropertySeparatorIndex(propertyPath, false); // 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. *

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) throws BeansException { if (this.nestedBeanWrappers == null) { this.nestedBeanWrappers = new HashMap(); } // get value of bean property PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); Object propertyValue = getPropertyValue(tokens); String canonicalName = tokens.canonicalName; String propertyName = tokens.actualName; 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.isDebugEnabled()) { logger.debug("Creating new nested BeanWrapper for property '" + canonicalName + "'"); } nestedBw = new BeanWrapperImpl(propertyValue, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR, this); // inherit all type-specific PropertyEditors if (this.customEditors != null) { for (Iterator it = this.customEditors.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); if (entry.getKey() instanceof Class) { Class requiredType = (Class) entry.getKey(); PropertyEditor editor = (PropertyEditor) entry.getValue(); nestedBw.registerCustomEditor(requiredType, editor); } else if (entry.getKey() instanceof String) { String editorPath = (String) entry.getKey(); int pos = getNestedPropertySeparatorIndex(editorPath, false); if (pos != -1) { String editorNestedProperty = editorPath.substring(0, pos); String editorNestedPath = editorPath.substring(pos + 1); if (editorNestedProperty.equals(canonicalName) || editorNestedProperty.equals(propertyName)) { CustomEditorHolder editorHolder = (CustomEditorHolder) entry.getValue(); nestedBw.registerCustomEditor( editorHolder.getRegisteredType(), editorNestedPath, editorHolder.getPropertyEditor()); } } } } } this.nestedBeanWrappers.put(canonicalName, nestedBw); } else { if (logger.isDebugEnabled()) { logger.debug("Using cached nested BeanWrapper for property '" + canonicalName + "'"); } } return nestedBw; } 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 = key.substring(1, key.length() - 1); } else if (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 = (String[]) keys.toArray(new String[keys.size()]); } return tokens; } public Object getPropertyValue(String propertyName) throws BeansException { BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName); PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName)); return nestedBw.getPropertyValue(tokens); } protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { String propertyName = tokens.canonicalName; String actualName = tokens.actualName; PropertyDescriptor pd = getPropertyDescriptorInternal(tokens.actualName); if (pd == null || pd.getReadMethod() == null) { throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName); } if (logger.isDebugEnabled()) logger.debug("About to invoke read method [" + pd.getReadMethod() + "] on object of class [" + this.object.getClass().getName() + "]"); try { Object value = pd.getReadMethod().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; value = map.get(key); } 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, value); } protected void setPropertyValue(PropertyTokenHolder tokens, Object value) throws BeansException { String propertyName = tokens.canonicalName; 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(); Object newValue = doTypeConversionIfNecessary(propertyName, propertyName, null, value, requiredType); try { Array.set(propValue, Integer.parseInt(key), newValue); } catch (IllegalArgumentException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, null, newValue); 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) { Object newValue = doTypeConversionIfNecessary(propertyName, propertyName, null, value, null); List list = (List) propValue; int index = Integer.parseInt(key); if (index < list.size()) { list.set(index, newValue); } 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(newValue); } } else if (propValue instanceof Map) { Object newValue = doTypeConversionIfNecessary(propertyName, propertyName, null, value, null); Map map = (Map) propValue; map.put(key, newValue); } 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 [" + value + "]"); } } else { if (!isWritableProperty(propertyName)) { throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName); } PropertyDescriptor pd = getPropertyDescriptor(propertyName); Method writeMethod = pd.getWriteMethod(); Object newValue = null; try { // old value may still be null newValue = doTypeConversionIfNecessary(propertyName, propertyName, null, value, pd.getPropertyType()); if (pd.getPropertyType().isPrimitive() && (newValue == null || "".equals(newValue))) { throw new IllegalArgumentException("Invalid value [" + value + "] for property '" + pd.getName() + "' of primitive type [" + pd.getPropertyType() + "]"); } if (logger.isDebugEnabled()) { logger.debug("About to invoke write method [" + writeMethod + "] on object of class [" + this.object.getClass().getName() + "]"); } writeMethod.invoke(this.object, new Object[] {newValue}); if (logger.isDebugEnabled()) { logger.debug("Invoked write method [" + writeMethod + "] with value of type [" + pd.getPropertyType().getName() + "]"); } } catch (InvocationTargetException ex) { PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, null, value); 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, null, value); throw new TypeMismatchException(pce, pd.getPropertyType(), ex); } catch (IllegalAccessException ex) { PropertyChangeEvent pce = new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, null, value); throw new MethodInvocationException(pce, ex); } } } public void setPropertyValue(PropertyValue pv) throws BeansException { setPropertyValue(pv.getName(), pv.getValue()); } /** * Bulk update from a Map. * Bulk updates from PropertyValues are more powerful: this method is * provided for convenience. * @param map map containing properties to set, as name-value pairs. * The map may include nested properties. * @throws BeansException if there's a fatal, low-level exception */ public void setPropertyValues(Map map) throws BeansException { setPropertyValues(new MutablePropertyValues(map)); } public void setPropertyValues(PropertyValues pvs) throws BeansException { setPropertyValues(pvs, false); } public void setPropertyValues(PropertyValues propertyValues, boolean ignoreUnknown) throws BeansException { List propertyAccessExceptions = new ArrayList(); PropertyValue[] pvs = propertyValues.getPropertyValues(); for (int i = 0; i < pvs.length; i++) { try { // This method may throw any BeansException, which won't be caught // here, if there is a critical failure such as no matching field. // We can attempt to deal only with less serious exceptions. setPropertyValue(pvs[i]); } catch (NotWritablePropertyException ex) { if (!ignoreUnknown) { throw ex; } // otherwise, just ignore it and continue... } catch (PropertyAccessException ex) { propertyAccessExceptions.add(ex); } } // If we encountered individual exceptions, throw the composite exception. if (!propertyAccessExceptions.isEmpty()) { Object[] paeArray = propertyAccessExceptions.toArray(new PropertyAccessException[propertyAccessExceptions.size()]); throw new PropertyAccessExceptionsException(this, (PropertyAccessException[]) paeArray); } } private PropertyChangeEvent createPropertyChangeEvent(String propertyName, Object oldValue, Object newValue) { return new PropertyChangeEvent((this.rootObject != null ? this.rootObject : "constructor"), (propertyName != null ? this.nestedPath + propertyName : null), oldValue, newValue); } /** * Convert the value to the required type (if necessary from a String). *

Conversions from String to any type use the setAsText method * of the PropertyEditor class. Note that a PropertyEditor must be registered * for the given class for this to work; this is a standard JavaBeans API. * A number of PropertyEditors are automatically registered by BeanWrapperImpl. * @param newValue proposed change value * @param requiredType the type we must convert to * @return the new value, possibly the result of type conversion * @throws TypeMismatchException if type conversion failed * @see java.beans.PropertyEditor#setAsText(String) * @see java.beans.PropertyEditor#getValue() */ public Object doTypeConversionIfNecessary(Object newValue, Class requiredType) throws TypeMismatchException { return doTypeConversionIfNecessary(null, null, null, newValue, requiredType); } /** * Convert the value to the required type (if necessary from a String), * for the specified property. * @param propertyName name of the property * @param oldValue previous value, if available (may be null) * @param newValue proposed change value * @param requiredType the type we must convert to * (or null if not known, for example in case of a collection element) * @return the new value, possibly the result of type conversion * @throws TypeMismatchException if type conversion failed */ protected Object doTypeConversionIfNecessary(String propertyName, String fullPropertyName, Object oldValue, Object newValue, Class requiredType) throws TypeMismatchException { Object convertedValue = newValue; if (convertedValue != null) { // Custom editor for this type? PropertyEditor pe = findCustomEditor(requiredType, fullPropertyName); // Value not of required type? if (pe != null || (requiredType != null && (requiredType.isArray() || !requiredType.isAssignableFrom(convertedValue.getClass())))) { if (requiredType != null) { if (pe == null) { // No custom editor -> check BeanWrapperImpl's default editors. pe = (PropertyEditor) this.defaultEditors.get(requiredType); if (pe == null) { // No BeanWrapper default editor -> check standard JavaBean editors. pe = PropertyEditorManager.findEditor(requiredType); } } } if (pe != null && !(convertedValue instanceof String)) { // Not a String -> use PropertyEditor's setValue. // With standard PropertyEditors, this will return the very same object; // we just want to allow special PropertyEditors to override setValue // for type conversion from non-String values to the required type. try { pe.setValue(convertedValue); convertedValue = pe.getValue(); } catch (IllegalArgumentException ex) { throw new TypeMismatchException( createPropertyChangeEvent(fullPropertyName, oldValue, newValue), requiredType, ex); } } if (requiredType != null && !requiredType.isArray() && convertedValue instanceof String[]) { // Convert String array to a comma-separated String. // Only applies if no PropertyEditor converted the String array before. // The CSV String will be passed into a PropertyEditor's setAsText method, if any. if (logger.isDebugEnabled()) { logger.debug("Converting String array to comma-delimited String [" + convertedValue + "]"); } convertedValue = StringUtils.arrayToCommaDelimitedString((String[]) convertedValue); } if (pe != null && convertedValue instanceof String) { // Use PropertyEditor's setAsText in case of a String value. if (logger.isDebugEnabled()) { logger.debug("Converting String to [" + requiredType + "] using property editor [" + pe + "]"); } try { pe.setAsText((String) convertedValue); convertedValue = pe.getValue(); } catch (IllegalArgumentException ex) { throw new TypeMismatchException( createPropertyChangeEvent(fullPropertyName, oldValue, newValue), requiredType, ex); } } if (requiredType != null) { // Array required -> apply appropriate conversion of elements. if (requiredType.isArray()) { Class componentType = requiredType.getComponentType(); if (convertedValue instanceof Collection) { // Convert Collection elements to array elements. Collection coll = (Collection) convertedValue; Object result = Array.newInstance(componentType, coll.size()); int i = 0; for (Iterator it = coll.iterator(); it.hasNext(); i++) { Object value = doTypeConversionIfNecessary( propertyName, propertyName + PROPERTY_KEY_PREFIX + i + PROPERTY_KEY_SUFFIX, null, it.next(), componentType); Array.set(result, i, value); } return result; } else if (convertedValue != null && convertedValue.getClass().isArray()) { // Convert Collection elements to array elements. int arrayLength = Array.getLength(convertedValue); Object result = Array.newInstance(componentType, arrayLength); for (int i = 0; i < arrayLength; i++) { Object value = doTypeConversionIfNecessary( propertyName, propertyName + PROPERTY_KEY_PREFIX + i + PROPERTY_KEY_SUFFIX, null, Array.get(convertedValue, i), componentType); Array.set(result, i, value); } return result; } else { // A plain value: convert it to an array with a single component. Object result = Array.newInstance(componentType, 1); Object val = doTypeConversionIfNecessary( propertyName, propertyName + PROPERTY_KEY_PREFIX + 0 + PROPERTY_KEY_SUFFIX, null, convertedValue, componentType); Array.set(result, 0, val); return result; } } // Throw explicit TypeMismatchException with full context information // if the resulting value definitely doesn't match the required type. if (convertedValue != null && !requiredType.isPrimitive() && !requiredType.isAssignableFrom(convertedValue.getClass())) { throw new TypeMismatchException( createPropertyChangeEvent(fullPropertyName, oldValue, newValue), requiredType); } } } } return convertedValue; } public PropertyDescriptor[] getPropertyDescriptors() { return this.cachedIntrospectionResults.getBeanInfo().getPropertyDescriptors(); } public PropertyDescriptor getPropertyDescriptor(String propertyName) throws BeansException { if (propertyName == null) { throw new IllegalArgumentException("Can't find property descriptor for null property"); } PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName); if (pd != null) { return pd; } else { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "No property '" + propertyName + "' found"); } } /** * Internal version of getPropertyDescriptor: * Returns null if not found rather than throwing an exception. */ protected PropertyDescriptor getPropertyDescriptorInternal(String propertyName) throws BeansException { Assert.state(this.object != null, "BeanWrapper does not hold a bean instance"); BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName); return nestedBw.cachedIntrospectionResults.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(); } } } catch (InvalidPropertyException ex) { // consider as not determinable } return null; } public boolean isReadableProperty(String propertyName) { // This is a programming error, although asking for a property // that doesn't exist is not. if (propertyName == null) { throw new IllegalArgumentException("Can't find readability status for null property"); } 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) { // This is a programming error, although asking for a property // that doesn't exist is not. if (propertyName == null) { throw new IllegalArgumentException("Can't find writability status for null property"); } 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; } //--------------------------------------------------------------------- // Diagnostics //--------------------------------------------------------------------- public String toString() { StringBuffer sb = new StringBuffer("BeanWrapperImpl: wrapping class ["); sb.append(getWrappedClass().getName()).append("]"); return sb.toString(); } /** * Holder for a registered custom editor with property name. * Keeps the PropertyEditor itself plus the type it was registered for. */ private static class CustomEditorHolder { private final PropertyEditor propertyEditor; private final Class registeredType; private CustomEditorHolder(PropertyEditor propertyEditor, Class registeredType) { this.propertyEditor = propertyEditor; this.registeredType = registeredType; } private PropertyEditor getPropertyEditor() { return propertyEditor; } private Class getRegisteredType() { return registeredType; } private PropertyEditor getPropertyEditor(Class requiredType) { // Special case: If no required type specified, which usually only happens for // Collection elements, or required type is not assignable to registered type, // which usually only happens for generic properties of type Object - // then return PropertyEditor if not registered for Collection or array type. // (If not registered for Collection or array, it is assumed to be intended // for elements.) if (this.registeredType == null || (requiredType != null && (BeanUtils.isAssignable(this.registeredType, requiredType) || BeanUtils.isAssignable(requiredType, this.registeredType))) || (requiredType == null && (!Collection.class.isAssignableFrom(this.registeredType) && !this.registeredType.isArray()))) { return this.propertyEditor; } else { return null; } } } private static class PropertyTokenHolder { private String canonicalName; private String actualName; private String[] keys; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy