Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 eu.qualityontime.commons; import java.beans.IndexedPropertyDescriptor; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.CopyOnWriteArrayList; import org.apache.commons.collections.FastHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import eu.qualityontime.commons.expression.DefaultResolver; import eu.qualityontime.commons.expression.Resolver; /** * Utility methods for using Java Reflection APIs to facilitate generic property * getter and setter operations on Java objects. Much of this code was * originally included in <code>BeanUtils</code>, but has been separated because * of the volume of code involved. * <p> * In general, the objects that are examined and modified using these methods * are expected to conform to the property getter and setter method naming * conventions described in the JavaBeans Specification (Version 1.0.1). No data * type conversions are performed, and there are no usage of any * <code>PropertyEditor</code> classes that have been registered, although a * convenient way to access the registered classes themselves is included. * <p> * For the purposes of this class, five formats for referencing a particular * property value of a bean are defined, with the <i>default</i> layout of an * identifying String in parentheses. However the notation for these formats and * how they are resolved is now (since BeanUtils 1.8.0) controlled by the * configured {@link Resolver} implementation: * <ul> * <li><strong>Simple (<code>name</code>)</strong> - The specified * <code>name</code> identifies an individual property of a particular JavaBean. * The name of the actual getter or setter method to be used is determined using * standard JavaBeans instrospection, so that (unless overridden by a * <code>BeanInfo</code> class, a property named "xyz" will have a getter method * named <code>getXyz()</code> or (for boolean properties only) * <code>isXyz()</code>, and a setter method named <code>setXyz()</code>.</li> * <li><strong>Nested (<code>name1.name2.name3</code>)</strong> The first name * element is used to select a property getter, as for simple references above. * The object returned for this property is then consulted, using the same * approach, for a property getter for a property named <code>name2</code>, and * so on. The property value that is ultimately retrieved or modified is the one * identified by the last name element.</li> * <li><strong>Indexed (<code>name[index]</code>)</strong> - The underlying * property value is assumed to be an array, or this JavaBean is assumed to have * indexed property getter and setter methods. The appropriate (zero-relative) * entry in the array is selected. <code>List</code> objects are now also * supported for read/write. You simply need to define a getter that returns the * <code>List</code></li> * <li><strong>Mapped (<code>name(key)</code>)</strong> - The JavaBean is * assumed to have an property getter and setter methods with an additional * attribute of type <code>java.lang.String</code>.</li> * <li><strong>Combined (<code>name1.name2[index].name3(key)</code>)</strong> - * Combining mapped, nested, and indexed references is also supported.</li> * </ul> * * @version $Id$ * @see Resolver * @since 1.7 */ public class QPropertyUtilsBean { private Resolver resolver = new DefaultResolver(); // --------------------------------------------------------- Variables /** * The cache of PropertyDescriptor arrays for beans we have already * introspected, keyed by the java.lang.Class of this object. */ private WeakFastHashMap<Class<?>, BeanIntrospectionData> descriptorsCache = null; private WeakFastHashMap<Class<?>, FastHashMap> mappedDescriptorsCache = null; /** An empty object array */ private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; /** Log instance */ private final Log log = LogFactory.getLog(QPropertyUtils.class); /** The list with BeanIntrospector objects. */ private final List<BeanIntrospector> introspectors; // ---------------------------------------------------------- Constructors /** Base constructor */ public QPropertyUtilsBean() { descriptorsCache = new WeakFastHashMap<Class<?>, BeanIntrospectionData>(); descriptorsCache.setFast(true); mappedDescriptorsCache = new WeakFastHashMap<Class<?>, FastHashMap>(); mappedDescriptorsCache.setFast(true); introspectors = new CopyOnWriteArrayList<BeanIntrospector>(); resetBeanIntrospectors(); } // --------------------------------------------------------- Public Methods /** * Return the configured {@link Resolver} implementation used by BeanUtils. * <p> * The {@link Resolver} handles the <i>property name</i> expressions and the * implementation in use effectively controls the dialect of the * <i>expression language</i> that BeanUtils recongnises. * <p> * {@link DefaultResolver} is the default implementation used. * * @return resolver The property expression resolver. * @since 1.8.0 */ public Resolver getResolver() { return resolver; } /** * Configure the {@link Resolver} implementation used by BeanUtils. * <p> * The {@link Resolver} handles the <i>property name</i> expressions and the * implementation in use effectively controls the dialect of the * <i>expression language</i> that BeanUtils recongnises. * <p> * {@link DefaultResolver} is the default implementation used. * * @param resolver * The property expression resolver. * @since 1.8.0 */ public void setResolver(final Resolver resolver) { if (resolver == null) { this.resolver = new DefaultResolver(); } else { this.resolver = resolver; } } /** * Resets the {@link BeanIntrospector} objects registered at this instance. * After this method was called, only the default {@code BeanIntrospector} * is registered. * * @since 1.9 */ public final void resetBeanIntrospectors() { introspectors.clear(); introspectors.add(DefaultBeanIntrospector.INSTANCE); } /** * Adds a <code>BeanIntrospector</code>. This object is invoked when the * property descriptors of a class need to be obtained. * * @param introspector * the <code>BeanIntrospector</code> to be added (must not be * <b>null</b> * @throws IllegalArgumentException * if the argument is <b>null</b> * @since 1.9 */ public void addBeanIntrospector(final BeanIntrospector introspector) { if (introspector == null) { throw new IllegalArgumentException("BeanIntrospector must not be null!"); } introspectors.add(introspector); } /** * Removes the specified <code>BeanIntrospector</code>. * * @param introspector * the <code>BeanIntrospector</code> to be removed * @return <b>true</b> if the <code>BeanIntrospector</code> existed and * could be removed, <b>false</b> otherwise * @since 1.9 */ public boolean removeBeanIntrospector(final BeanIntrospector introspector) { return introspectors.remove(introspector); } /** * Clear any cached property descriptors information for all classes loaded * by any class loaders. This is useful in cases where class loaders are * thrown away to implement class reloading. */ public void clearDescriptors() { descriptorsCache.clear(); mappedDescriptorsCache.clear(); Introspector.flushCaches(); } /** * <p> * Copy property values from the "origin" bean to the "destination" bean for * all cases where the property names are the same (even though the actual * getter and setter methods might have been customized via * <code>BeanInfo</code> classes). No conversions are performed on the * actual property values -- it is assumed that the values retrieved from * the origin bean are assignment-compatible with the types expected by the * destination bean. * </p> * * <p> * If the origin "bean" is actually a <code>Map</code>, it is assumed to * contain String-valued <strong>simple</strong> property names as the keys, * pointing at the corresponding property values that will be set in the * destination bean.<strong>Note</strong> that this method is intended to * perform a "shallow copy" of the properties and so complex properties (for * example, nested ones) will not be copied. * </p> * * <p> * Note, that this method will not copy a List to a List, or an Object[] to * an Object[]. It's specifically for copying JavaBean properties. * </p> * * @param dest * Destination bean whose properties are modified * @param orig * Origin bean whose properties are retrieved * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if the <code>dest</code> or <code>orig</code> argument is * null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public void copyProperties(final Object dest, final Object orig) { try { _copyProperties(dest, orig); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public void _copyProperties(final Object dest, final Object orig) throws Exception { if (dest == null) { throw new IllegalArgumentException("No destination bean specified"); } if (orig == null) { throw new IllegalArgumentException("No origin bean specified"); } if (orig instanceof Map) { final Iterator<?> entries = ((Map<?, ?>) orig).entrySet().iterator(); while (entries.hasNext()) { final Map.Entry<?, ?> entry = (Entry<?, ?>) entries.next(); final String name = (String) entry.getKey(); if (isWriteable(dest, name)) { try { _setSimpleProperty(dest, name, entry.getValue()); } catch (final NoSuchMethodException e) { if (log.isDebugEnabled()) { log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e); } } } } } else /* if (orig is a standard JavaBean) */ { final PropertyDescriptor[] origDescriptors = getPropertyDescriptors(orig); for (final PropertyDescriptor origDescriptor : origDescriptors) { final String name = origDescriptor.getName(); if (isReadable(orig, name) && isWriteable(dest, name)) { try { final Object value = getSimpleProperty(orig, name); { _setSimpleProperty(dest, name, value); } } catch (final NoSuchMethodException e) { if (log.isDebugEnabled()) { log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e); } } } } } } /** * <p> * Return the entire set of properties for which the specified bean provides * a read method. This map contains the unconverted property values for all * properties for which a read method is provided (i.e. where the * <code>getReadMethod()</code> returns non-null). * </p> * * <p> * <strong>FIXME</strong> - Does not account for mapped properties. * </p> * * @param bean * Bean whose properties are to be extracted * @return The set of properties for the bean * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> is null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Map<String, Object> describe(final Object bean) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } final Map<String, Object> description = new HashMap<String, Object>(); { final PropertyDescriptor[] descriptors = getPropertyDescriptors(bean); for (final PropertyDescriptor descriptor : descriptors) { final String name = descriptor.getName(); if (descriptor.getReadMethod() != null) { description.put(name, getProperty(bean, name)); } } } return description; } /** * Return the value of the specified indexed property of the specified bean, * with no type conversions. The zero-relative index of the required value * must be included (in square brackets) as a suffix to the property name, * or <code>IllegalArgumentException</code> will be thrown. In addition to * supporting the JavaBeans specification, this method has been extended to * support <code>List</code> objects as well. * * @param bean * Bean whose property is to be extracted * @param name * <code>propertyname[index]</code> of the property value to be * extracted * @return the indexed property value * * @throws IndexOutOfBoundsException * if the specified index is outside the valid range for the * underlying array or List * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Object getIndexedProperty(final Object bean, String name) { try { return _getIndexedProperty(bean, name); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public Object _getIndexedProperty(final Object bean, String name) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Identify the index of the requested individual property int index = -1; try { index = resolver.getIndex(name); } catch (final IllegalArgumentException e) { throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "' " + e.getMessage()); } if (index < 0) { throw new IllegalArgumentException( "Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'"); } // Isolate the name name = resolver.getProperty(name); // Request the specified indexed property value return _getIndexedProperty(bean, name, index); } /** * Return the value of the specified indexed property of the specified bean, * with no type conversions. In addition to supporting the JavaBeans * specification, this method has been extended to support <code>List</code> * objects as well. * * @param bean * Bean whose property is to be extracted * @param name * Simple property name of the property value to be extracted * @param index * Index of the property value to be extracted * @return the indexed property value * * @throws IndexOutOfBoundsException * if the specified index is outside the valid range for the * underlying property * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Object getIndexedProperty(final Object bean, final String name, final int index) { try { return _getIndexedProperty(bean, name, index); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public Object _getIndexedProperty(final Object bean, final String name, final int index) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null || name.length() == 0) { if (bean.getClass().isArray()) { return Array.get(bean, index); } else if (bean instanceof List) { return ((List<?>) bean).get(index); } } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } if (name.startsWith("@")) { Object f = FieldUtils.readField(bean, trimAnnotations(name)); if (null == f) { if (name.endsWith("?")) return null; else throw new NestedNullException(); } if (f.getClass().isArray()) return Array.get(f, index); else if (f instanceof List) return ((List<?>) f).get(index); } // Retrieve the property descriptor for the specified property final PropertyDescriptor descriptor = getPropertyDescriptor(bean, trimAnnotations(name)); if (descriptor == null) { throw new NoSuchMethodException( "Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); } // Call the indexed getter method if there is one if (descriptor instanceof IndexedPropertyDescriptor) { Method readMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedReadMethod(); readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod); if (readMethod != null) { final Object[] subscript = new Object[1]; subscript[0] = new Integer(index); try { return invokeMethod(readMethod, bean, subscript); } catch (final InvocationTargetException e) { if (e.getTargetException() instanceof IndexOutOfBoundsException) { throw (IndexOutOfBoundsException) e.getTargetException(); } else { throw e; } } } } // Otherwise, the underlying property must be an array final Method readMethod = getReadMethod(bean.getClass(), descriptor); if (readMethod == null) { throw new NoSuchMethodException( "Property '" + name + "' has no " + "getter method on bean class '" + bean.getClass() + "'"); } // Call the property getter and return the value final Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY); if (null == value && name.endsWith("?")) return null; if (!value.getClass().isArray()) { if (!(value instanceof java.util.List)) { throw new IllegalArgumentException( "Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'"); } else { // get the List's value return ((java.util.List<?>) value).get(index); } } else { // get the array's value try { return Array.get(value, index); } catch (final ArrayIndexOutOfBoundsException e) { throw new ArrayIndexOutOfBoundsException( "Index: " + index + ", Size: " + Array.getLength(value) + " for property '" + name + "'"); } } } /** * Return the value of the specified mapped property of the specified bean, * with no type conversions. The key of the required value must be included * (in brackets) as a suffix to the property name, or * <code>IllegalArgumentException</code> will be thrown. * * @param bean * Bean whose property is to be extracted * @param name * <code>propertyname(key)</code> of the property value to be * extracted * @return the mapped property value * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Object getMappedProperty(final Object bean, String name) throws Exception { return _getMappedProperty(bean, name); } public Object _getMappedProperty(final Object bean, String name) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Identify the key of the requested individual property String key = null; try { key = resolver.getKey(name); } catch (final IllegalArgumentException e) { throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "' " + e.getMessage()); } if (key == null) { throw new IllegalArgumentException( "Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'"); } // Isolate the name name = resolver.getProperty(name); // Request the specified indexed property value return _getMappedProperty(bean, name, key); } /** * Return the value of the specified mapped property of the specified bean, * with no type conversions. * * @param bean * Bean whose property is to be extracted * @param name * Mapped property name of the property value to be extracted * @param key * Key of the property value to be extracted * @return the mapped property value * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Object getMappedProperty(final Object bean, final String name, final String key) { try { return _getMappedProperty(bean, name, key); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { throw new RuntimeException(e); } } public Object _getMappedProperty(final Object bean, final String name, final String key) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } if (key == null) { throw new IllegalArgumentException( "No key specified for property '" + name + "' on bean class " + bean.getClass() + "'"); } Object result = null; if (name.startsWith("@")) { Object invokeResult = FieldUtils.readField(bean, trimAnnotations(name)); if (null == invokeResult) { if (name.endsWith("?")) return null; else throw new NestedNullException(); } if (invokeResult instanceof java.util.Map) { return ((java.util.Map<?, ?>) invokeResult).get(key); } else throw new NoSuchFieldException("Field '" + name + "' is not mapped in '" + bean.getClass() + "'"); } // Retrieve the property descriptor for the specified property final PropertyDescriptor descriptor = getPropertyDescriptor(bean, trimAnnotations(name)); if (descriptor == null) { throw new NoSuchMethodException( "Unknown property '" + name + "'+ on bean class '" + bean.getClass() + "'"); } else if (descriptor instanceof MappedPropertyDescriptor) { // Call the keyed getter method if there is one Method readMethod = ((MappedPropertyDescriptor) descriptor).getMappedReadMethod(); readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod); if (readMethod != null) { final Object[] keyArray = new Object[1]; keyArray[0] = key; result = invokeMethod(readMethod, bean, keyArray); } else { throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'"); } } else { /* means that the result has to be retrieved from a map */ final Method readMethod = getReadMethod(bean.getClass(), descriptor); if (readMethod != null) { final Object invokeResult = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY); /* test and fetch from the map */ if (invokeResult instanceof java.util.Map) { result = ((java.util.Map<?, ?>) invokeResult).get(key); } } else { throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'"); } } return result; } /** * Trimming annotations fromt he beggining or the end of the attribute/field name */ private String trimAnnotations(String name) { if (name.startsWith("@")) name = name.substring(1); if (name.endsWith("?")) name = name.substring(0, name.length() - 1); return name; } /** * <p> * Return the mapped property descriptors for this bean class. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param beanClass * Bean class to be introspected * @return the mapped property descriptors * @deprecated This method should not be exposed */ @Deprecated public FastHashMap getMappedPropertyDescriptors(final Class<?> beanClass) { if (beanClass == null) { return null; } // Look up any cached descriptors for this bean class return mappedDescriptorsCache.get(beanClass); } /** * <p> * Return the mapped property descriptors for this bean. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param bean * Bean to be introspected * @return the mapped property descriptors * @deprecated This method should not be exposed */ @Deprecated public FastHashMap getMappedPropertyDescriptors(final Object bean) { if (bean == null) { return null; } return getMappedPropertyDescriptors(bean.getClass()); } /** * Return the value of the (possibly nested) property of the specified name, * for the specified bean, with no type conversions. * * @param bean * Bean whose property is to be extracted * @param name * Possibly nested name of the property to be extracted * @return the nested property value * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws NestedNullException * if a nested reference to a property returns null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Object getNestedProperty(final Object bean, final String name) { try { return _getNestedProperty(bean, name); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { throw new RuntimeException(e); } } public Object _getNestedProperty(Object bean, String name) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Resolve nested references while (resolver.hasNested(name)) { final String next = resolver.next(name); Object nestedBean = null; if (bean instanceof Map) { nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next); } else if (resolver.isMapped(next)) { nestedBean = getMappedProperty(bean, next); } else if (resolver.isIndexed(next)) { nestedBean = _getIndexedProperty(bean, next); } else { nestedBean = _getSimpleProperty(bean, next); } if (nestedBean == null) { if (resolver.isMapped(next)) { String prop = resolver.getProperty(next); if (prop.endsWith("?")) return null; } else if (resolver.isIndexed(next)) { String prop = resolver.getProperty(next); if (prop.endsWith("?")) return null; } if (next.endsWith("?")) return null; else throw new NestedNullException( "Null property value for '" + name + "' on bean class '" + bean.getClass() + "'"); } bean = nestedBean; name = resolver.remove(name); } if (bean instanceof Map) { bean = getPropertyOfMapBean((Map<?, ?>) bean, name); } else if (resolver.isMapped(name)) { bean = getMappedProperty(bean, name); } else if (resolver.isIndexed(name)) { bean = getIndexedProperty(bean, name); } else { bean = getSimpleProperty(bean, name); } return bean; } /** * This method is called by getNestedProperty and setNestedProperty to * define what it means to get a property from an object which implements * Map. See setPropertyOfMapBean for more information. * * @param bean * Map bean * @param propertyName * The property name * @return the property value * * @throws IllegalArgumentException * when the propertyName is regarded as being invalid. * * @throws IllegalAccessException * just in case subclasses override this method to try to access * real getter methods and find permission is denied. * * @throws InvocationTargetException * just in case subclasses override this method to try to access * real getter methods, and find it throws an exception when * invoked. * * @throws NoSuchMethodException * just in case subclasses override this method to try to access * real getter methods, and want to fail if no simple method is * available. * @since 1.8.0 */ protected Object getPropertyOfMapBean(final Map<?, ?> bean, String propertyName) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { if (resolver.isMapped(propertyName)) { final String name = resolver.getProperty(propertyName); if (name == null || name.length() == 0) { propertyName = resolver.getKey(propertyName); } } if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) { throw new IllegalArgumentException( "Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName); } return bean.get(propertyName); } /** * Return the value of the specified property of the specified bean, no * matter which property reference format is used, with no type conversions. * * @param bean * Bean whose property is to be extracted * @param name * Possibly indexed and/or nested name of the property to be * extracted * @return the property value * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Object getProperty(final Object bean, final String name) { try { return _getNestedProperty(bean, name); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { throw new RuntimeException(e); } } public Object _getProperty(final Object bean, final String name) throws Exception { return _getNestedProperty(bean, name); } /** * <p> * Retrieve the property descriptor for the specified property of the * specified bean, or return <code>null</code> if there is no such * descriptor. This method resolves indexed and nested property references * in the same manner as other methods in this class, except that if the * last (or only) name element is indexed, the descriptor for the last * resolved property itself is returned. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * <p> * Note that for Java 8 and above, this method no longer return * IndexedPropertyDescriptor for {@link List}-typed properties, only for * properties typed as native array. (BEANUTILS-492). * * @param bean * Bean for which a property descriptor is requested * @param name * Possibly indexed and/or nested name of the property for which * a property descriptor is requested * @return the property descriptor * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws IllegalArgumentException * if a nested reference to a property returns null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public PropertyDescriptor getPropertyDescriptor(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Resolve nested references while (resolver.hasNested(name)) { final String next = resolver.next(name); final Object nestedBean = getProperty(bean, next); if (nestedBean == null) { throw new NestedNullException( "Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); } bean = nestedBean; name = resolver.remove(name); } // Remove any subscript from the final name value name = resolver.getProperty(name); // Look up and return this property from our cache // creating and adding it to the cache if not found. if (name == null) { return null; } final BeanIntrospectionData data = getIntrospectionData(bean.getClass()); PropertyDescriptor result = data.getDescriptor(name); if (result != null) { return result; } FastHashMap mappedDescriptors = getMappedPropertyDescriptors(bean); if (mappedDescriptors == null) { mappedDescriptors = new FastHashMap(); mappedDescriptors.setFast(true); mappedDescriptorsCache.put(bean.getClass(), mappedDescriptors); } result = (PropertyDescriptor) mappedDescriptors.get(name); if (result == null) { // not found, try to create it try { result = new MappedPropertyDescriptor(name, bean.getClass()); } catch (final IntrospectionException ie) { /* * Swallow IntrospectionException TODO: Why? */ } if (result != null) { mappedDescriptors.put(name, result); } } return result; } /** * <p> * Retrieve the property descriptors for the specified class, introspecting * and caching them the first time a particular bean class is encountered. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param beanClass * Bean class for which property descriptors are requested * @return the property descriptors * * @throws IllegalArgumentException * if <code>beanClass</code> is null */ public PropertyDescriptor[] getPropertyDescriptors(final Class<?> beanClass) { return getIntrospectionData(beanClass).getDescriptors(); } /** * <p> * Retrieve the property descriptors for the specified bean, introspecting * and caching them the first time a particular bean class is encountered. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param bean * Bean for which property descriptors are requested * @return the property descriptors * * @throws IllegalArgumentException * if <code>bean</code> is null */ public PropertyDescriptor[] getPropertyDescriptors(final Object bean) { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } return getPropertyDescriptors(bean.getClass()); } /** * <p> * Return the Java Class repesenting the property editor class that has been * registered for this property (if any). This method follows the same name * resolution rules used by <code>getPropertyDescriptor()</code>, so if the * last element of a name reference is indexed, the property editor for the * underlying property's class is returned. * </p> * * <p> * Note that <code>null</code> will be returned if there is no property, or * if there is no registered property editor class. Because this return * value is ambiguous, you should determine the existence of the property * itself by other means. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param bean * Bean for which a property descriptor is requested * @param name * Possibly indexed and/or nested name of the property for which * a property descriptor is requested * @return the property editor class * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws IllegalArgumentException * if a nested reference to a property returns null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Class<?> getPropertyEditorClass(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); if (descriptor != null) { return descriptor.getPropertyEditorClass(); } else { return null; } } /** * Return the Java Class representing the property type of the specified * property, or <code>null</code> if there is no such property for the * specified bean. This method follows the same name resolution rules used * by <code>getPropertyDescriptor()</code>, so if the last element of a name * reference is indexed, the type of the property itself will be returned. * If the last (or only) element has no property with the specified name, * <code>null</code> is returned. * <p> * If the property is an indexed property (e.g. <code>String[]</code>), this * method will return the type of the items within that array. Note that * from Java 8 and newer, this method do not support such index types from * items within an Collection, and will instead return the collection type * (e.g. java.util.List) from the getter mtethod. * * @param bean * Bean for which a property descriptor is requested * @param name * Possibly indexed and/or nested name of the property for which * a property descriptor is requested * @return The property type * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws IllegalArgumentException * if a nested reference to a property returns null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Class<?> getPropertyType(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Resolve nested references while (resolver.hasNested(name)) { final String next = resolver.next(name); final Object nestedBean = getProperty(bean, next); if (nestedBean == null) { throw new NestedNullException( "Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); } bean = nestedBean; name = resolver.remove(name); } // Remove any subscript from the final name value name = resolver.getProperty(name); final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); if (descriptor == null) { return null; } else if (descriptor instanceof IndexedPropertyDescriptor) { return ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType(); } else if (descriptor instanceof MappedPropertyDescriptor) { return ((MappedPropertyDescriptor) descriptor).getMappedPropertyType(); } else { return descriptor.getPropertyType(); } } /** * <p> * Return an accessible property getter method for this property, if there * is one; otherwise return <code>null</code>. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param descriptor * Property descriptor to return a getter for * @return The read method */ public Method getReadMethod(final PropertyDescriptor descriptor) { return MethodUtils.getAccessibleMethod(descriptor.getReadMethod()); } /** * <p> * Return an accessible property getter method for this property, if there * is one; otherwise return <code>null</code>. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param clazz * The class of the read method will be invoked on * @param descriptor * Property descriptor to return a getter for * @return The read method */ Method getReadMethod(final Class<?> clazz, final PropertyDescriptor descriptor) { return MethodUtils.getAccessibleMethod(clazz, descriptor.getReadMethod()); } /** * Return the value of the specified simple property of the specified bean, * with no type conversions. * * @param bean * Bean whose property is to be extracted * @param name * Name of the property to be extracted * @return The property value * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws IllegalArgumentException * if the property name is nested or indexed * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public Object getSimpleProperty(final Object bean, final String name) { try { return _getSimpleProperty(bean, name); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { throw new RuntimeException(e); } } public Object _getSimpleProperty(final Object bean, final String name) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Validate the syntax of the property name if (resolver.hasNested(name)) { throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } else if (resolver.isIndexed(name)) { throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } else if (resolver.isMapped(name)) { throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } if (name.startsWith("@")) { final String fieldName = trimAnnotations(name); final Field f = FieldUtils.findField(bean.getClass(), fieldName); if (null == f) { throw new NoSuchFieldException("field `" + fieldName + "` not found"); } f.setAccessible(true); return f.get(bean); } else { // Retrieve the property getter method for the specified property final PropertyDescriptor descriptor = getPropertyDescriptor(bean, trimAnnotations(name)); if (descriptor == null) { throw new NoSuchMethodException( "Unknown property '" + name + "' on class '" + bean.getClass() + "'"); } final Method readMethod = getReadMethod(bean.getClass(), descriptor); if (readMethod == null) { throw new NoSuchMethodException( "Property '" + name + "' has no getter method in class '" + bean.getClass() + "'"); } // Call the property getter and return the value final Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY); return value; } } /** * <p> * Return an accessible property setter method for this property, if there * is one; otherwise return <code>null</code>. * </p> * * <p> * <em>Note:</em> This method does not work correctly with custom bean * introspection under certain circumstances. It may return {@code null} * even if a write method is defined for the property in question. Use * {@link #getWriteMethod(Class, PropertyDescriptor)} to be sure that the * correct result is returned. * </p> * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param descriptor * Property descriptor to return a setter for * @return The write method */ public Method getWriteMethod(final PropertyDescriptor descriptor) { return MethodUtils.getAccessibleMethod(descriptor.getWriteMethod()); } /** * <p> * Return an accessible property setter method for this property, if there * is one; otherwise return <code>null</code>. * </p> * * <p> * <strong>FIXME</strong> - Does not work with DynaBeans. * </p> * * @param clazz * The class of the read method will be invoked on * @param descriptor * Property descriptor to return a setter for * @return The write method * @since 1.9.1 */ public Method getWriteMethod(final Class<?> clazz, final PropertyDescriptor descriptor) { final BeanIntrospectionData data = getIntrospectionData(clazz); return MethodUtils.getAccessibleMethod(clazz, data.getWriteMethod(clazz, descriptor)); } /** * <p> * Return <code>true</code> if the specified property name identifies a * readable property on the specified bean; otherwise, return * <code>false</code>. * * @param bean * Bean to be examined * @param name * Property name to be evaluated * @return <code>true</code> if the property is readable, otherwise * <code>false</code> * * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is * <code>null</code> * * @since BeanUtils 1.6 */ public boolean isReadable(Object bean, String name) { // Validate method parameters if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Resolve nested references while (resolver.hasNested(name)) { final String next = resolver.next(name); Object nestedBean = null; try { nestedBean = getProperty(bean, next); } catch (final Exception e) { return false; } if (nestedBean == null) { throw new NestedNullException( "Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); } bean = nestedBean; name = resolver.remove(name); } // Remove any subscript from the final name value name = resolver.getProperty(name); // Return the requested result { try { final PropertyDescriptor desc = getPropertyDescriptor(bean, name); if (desc != null) { Method readMethod = getReadMethod(bean.getClass(), desc); if (readMethod == null) { if (desc instanceof IndexedPropertyDescriptor) { readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod(); } else if (desc instanceof MappedPropertyDescriptor) { readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod(); } readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod); } return readMethod != null; } else { return false; } } catch (final IllegalAccessException e) { return false; } catch (final InvocationTargetException e) { return false; } catch (final NoSuchMethodException e) { return false; } } } /** * <p> * Return <code>true</code> if the specified property name identifies a * writeable property on the specified bean; otherwise, return * <code>false</code>. * * @param bean * Bean to be examined (may be a * @param name * Property name to be evaluated * @return <code>true</code> if the property is writeable, otherwise * <code>false</code> * * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is * <code>null</code> * * @since BeanUtils 1.6 */ public boolean isWriteable(Object bean, String name) { // Validate method parameters if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Resolve nested references while (resolver.hasNested(name)) { final String next = resolver.next(name); Object nestedBean = null; try { nestedBean = getProperty(bean, next); } catch (final Exception e) { return false; } if (nestedBean == null) { throw new NestedNullException( "Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); } bean = nestedBean; name = resolver.remove(name); } // Remove any subscript from the final name value name = resolver.getProperty(name); { try { final PropertyDescriptor desc = getPropertyDescriptor(bean, name); if (desc != null) { Method writeMethod = getWriteMethod(bean.getClass(), desc); if (writeMethod == null) { if (desc instanceof IndexedPropertyDescriptor) { writeMethod = ((IndexedPropertyDescriptor) desc).getIndexedWriteMethod(); } else if (desc instanceof MappedPropertyDescriptor) { writeMethod = ((MappedPropertyDescriptor) desc).getMappedWriteMethod(); } writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod); } return writeMethod != null; } else { return false; } } catch (final IllegalAccessException e) { return false; } catch (final InvocationTargetException e) { return false; } catch (final NoSuchMethodException e) { return false; } } } /** * Set the value of the specified indexed property of the specified bean, * with no type conversions. The zero-relative index of the required value * must be included (in square brackets) as a suffix to the property name, * or <code>IllegalArgumentException</code> will be thrown. In addition to * supporting the JavaBeans specification, this method has been extended to * support <code>List</code> objects as well. * * @param bean * Bean whose property is to be modified * @param name * <code>propertyname[index]</code> of the property value to be * modified * @param value * Value to which the specified property element should be set * * @throws IndexOutOfBoundsException * if the specified index is outside the valid range for the * underlying property * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public void setIndexedProperty(final Object bean, String name, final Object value) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { try { _setIndexedProperty(bean, name, value); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public void _setIndexedProperty(final Object bean, String name, final Object value) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Identify the index of the requested individual property int index = -1; try { index = resolver.getIndex(name); } catch (final IllegalArgumentException e) { throw new IllegalArgumentException( "Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'"); } if (index < 0) { throw new IllegalArgumentException( "Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'"); } // Isolate the name name = resolver.getProperty(name); // Set the specified indexed property value _setIndexedProperty(bean, name, index, value); } /** * Set the value of the specified indexed property of the specified bean, * with no type conversions. In addition to supporting the JavaBeans * specification, this method has been extended to support <code>List</code> * objects as well. * * @param bean * Bean whose property is to be set * @param name * Simple property name of the property value to be set * @param index * Index of the property value to be set * @param value * Value to which the indexed property element is to be set * * @throws IndexOutOfBoundsException * if the specified index is outside the valid range for the * underlying property * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public void setIndexedProperty(final Object bean, final String name, final int index, final Object value) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { try { _setIndexedProperty(bean, name, index, value); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public void _setIndexedProperty(final Object bean, final String name, final int index, final Object value) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null || name.length() == 0) { if (bean.getClass().isArray()) { Array.set(bean, index, value); return; } else if (bean instanceof List) { final List<Object> list = toObjectList(bean); list.set(index, value); return; } } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } if (name.startsWith("@")) { Object f = FieldUtils.readField(bean, name.substring(1)); if (null == f) throw new NestedNullException(); if (f.getClass().isArray()) { Array.set(f, index, value); return; } else if (f instanceof List) { final List<Object> list = toObjectList(f); list.set(index, value); return; } return; } // Retrieve the property descriptor for the specified property final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); if (descriptor == null) { throw new NoSuchMethodException( "Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); } // Call the indexed setter method if there is one if (descriptor instanceof IndexedPropertyDescriptor) { Method writeMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod(); writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod); if (writeMethod != null) { final Object[] subscript = new Object[2]; subscript[0] = new Integer(index); subscript[1] = value; try { if (log.isTraceEnabled()) { final String valueClassName = value == null ? "<null>" : value.getClass().getName(); log.trace("setSimpleProperty: Invoking method " + writeMethod + " with index=" + index + ", value=" + value + " (class " + valueClassName + ")"); } invokeMethod(writeMethod, bean, subscript); } catch (final InvocationTargetException e) { if (e.getTargetException() instanceof IndexOutOfBoundsException) { throw (IndexOutOfBoundsException) e.getTargetException(); } else { throw e; } } return; } } // Otherwise, the underlying property must be an array or a list final Method readMethod = getReadMethod(bean.getClass(), descriptor); if (readMethod == null) { throw new NoSuchMethodException( "Property '" + name + "' has no getter method on bean class '" + bean.getClass() + "'"); } // Call the property getter to get the array or list final Object array = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY); if (!array.getClass().isArray()) { if (array instanceof List) { // Modify the specified value in the List final List<Object> list = toObjectList(array); list.set(index, value); } else { throw new IllegalArgumentException( "Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'"); } } else { // Modify the specified value in the array Array.set(array, index, value); } } /** * Set the value of the specified mapped property of the specified bean, * with no type conversions. The key of the value to set must be included * (in brackets) as a suffix to the property name, or * <code>IllegalArgumentException</code> will be thrown. * * @param bean * Bean whose property is to be set * @param name * <code>propertyname(key)</code> of the property value to be set * @param value * The property value to be set * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public void setMappedProperty(final Object bean, String name, final Object value) { try { _setMappedProperty(bean, name, value); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public void _setMappedProperty(final Object bean, String name, final Object value) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Identify the key of the requested individual property String key = null; try { key = resolver.getKey(name); } catch (final IllegalArgumentException e) { throw new IllegalArgumentException( "Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'"); } if (key == null) { throw new IllegalArgumentException( "Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'"); } // Isolate the name name = resolver.getProperty(name); // Request the specified indexed property value _setMappedProperty(bean, name, key, value); } /** * Set the value of the specified mapped property of the specified bean, * with no type conversions. * * @param bean * Bean whose property is to be set * @param name * Mapped property name of the property value to be set * @param key * Key of the property value to be set * @param value * The property value to be set * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found * @throws NoSuchFieldException */ public void setMappedProperty(final Object bean, final String name, final String key, final Object value) { try { _setMappedProperty(bean, name, key, value); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public void _setMappedProperty(final Object bean, final String name, final String key, final Object value) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } if (key == null) { throw new IllegalArgumentException( "No key specified for property '" + name + "' on bean class '" + bean.getClass() + "'"); } if (name.startsWith("@")) { Object f = FieldUtils.readField(bean, name.substring(1)); if (null == f) throw new NestedNullException(); if (f instanceof Map) { final java.util.Map<String, Object> map = toPropertyMap(f); map.put(key, value); return; } throw new NoSuchFieldException( "Field '" + name + "' is not mapped " + "on bean class '" + bean.getClass() + "'"); } // Retrieve the property descriptor for the specified property final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); if (descriptor == null) { throw new NoSuchMethodException( "Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); } if (descriptor instanceof MappedPropertyDescriptor) { // Call the keyed setter method if there is one Method mappedWriteMethod = ((MappedPropertyDescriptor) descriptor).getMappedWriteMethod(); mappedWriteMethod = MethodUtils.getAccessibleMethod(bean.getClass(), mappedWriteMethod); if (mappedWriteMethod != null) { final Object[] params = new Object[2]; params[0] = key; params[1] = value; if (log.isTraceEnabled()) { final String valueClassName = value == null ? "<null>" : value.getClass().getName(); log.trace("setSimpleProperty: Invoking method " + mappedWriteMethod + " with key=" + key + ", value=" + value + " (class " + valueClassName + ")"); } invokeMethod(mappedWriteMethod, bean, params); } else { throw new NoSuchMethodException("Property '" + name + "' has no mapped setter method" + "on bean class '" + bean.getClass() + "'"); } } else { /* means that the result has to be retrieved from a map */ final Method readMethod = getReadMethod(bean.getClass(), descriptor); if (readMethod != null) { final Object invokeResult = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY); /* test and fetch from the map */ if (invokeResult instanceof java.util.Map) { final java.util.Map<String, Object> map = toPropertyMap(invokeResult); map.put(key, value); } } else { throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'"); } } } /** * Set the value of the (possibly nested) property of the specified name, * for the specified bean, with no type conversions. * <p> * Example values for parameter "name" are: * <ul> * <li>"a" -- sets the value of property a of the specified bean</li> * <li>"a.b" -- gets the value of property a of the specified bean, then on * that object sets the value of property b.</li> * <li>"a(key)" -- sets a value of mapped-property a on the specified bean. * This effectively means bean.setA("key").</li> * <li>"a[3]" -- sets a value of indexed-property a on the specified bean. * This effectively means bean.setA(3).</li> * </ul> * * @param bean * Bean whose property is to be modified * @param name * Possibly nested name of the property to be modified * @param value * Value to which the property is to be set * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws IllegalArgumentException * if a nested reference to a property returns null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public void setNestedProperty(Object bean, String name, final Object value) { try { _setNestedProperty(bean, name, value); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public void _setNestedProperty(Object bean, String name, final Object value) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Resolve nested references while (resolver.hasNested(name)) { final String next = resolver.next(name); Object nestedBean = null; if (bean instanceof Map) { nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next); } else if (resolver.isMapped(next)) { nestedBean = getMappedProperty(bean, next); } else if (resolver.isIndexed(next)) { nestedBean = _getIndexedProperty(bean, next); } else { nestedBean = _getSimpleProperty(bean, next); } if (nestedBean == null) { throw new NestedNullException( "Null property value for '" + name + "' on bean class '" + bean.getClass() + "'"); } bean = nestedBean; name = resolver.remove(name); } if (bean instanceof Map) { setPropertyOfMapBean(toPropertyMap(bean), name, value); } else if (resolver.isMapped(name)) { _setMappedProperty(bean, name, value); } else if (resolver.isIndexed(name)) { _setIndexedProperty(bean, name, value); } else { _setSimpleProperty(bean, name, value); } } /** * This method is called by method setNestedProperty when the current bean * is found to be a Map object, and defines how to deal with setting a * property on a Map. * <p> * The standard implementation here is to: * <ul> * <li>call bean.set(propertyName) for all propertyName values.</li> * <li>throw an IllegalArgumentException if the property specifier contains * MAPPED_DELIM or INDEXED_DELIM, as Map entries are essentially simple * properties; mapping and indexing operations do not make sense when * accessing a map (even thought the returned object may be a Map or an * Array).</li> * </ul> * <p> * The default behaviour of beanutils 1.7.1 or later is for assigning to * "a.b" to mean a.put(b, obj) always. However the behaviour of beanutils * version 1.6.0, 1.6.1, 1.7.0 was for "a.b" to mean a.setB(obj) if such a * method existed, and a.put(b, obj) otherwise. In version 1.5 it meant * a.put(b, obj) always (ie the same as the behaviour in the current * version). In versions prior to 1.5 it meant a.setB(obj) always. [yes, * this is all <i>very</i> unfortunate] * <p> * Users who would like to customise the meaning of "a.b" in method * setNestedProperty when a is a Map can create a custom subclass of this * class and override this method to implement the behaviour of their * choice, such as restoring the pre-1.4 behaviour of this class if they * wish. When overriding this method, do not forget to deal with * MAPPED_DELIM and INDEXED_DELIM characters in the propertyName. * <p> * Note, however, that the recommended solution for objects that implement * Map but want their simple properties to come first is for <i>those</i> * objects to override their get/put methods to implement that behaviour, * and <i>not</i> to solve the problem by modifying the default behaviour of * the PropertyUtilsBean class by overriding this method. * * @param bean * Map bean * @param propertyName * The property name * @param value * the property value * * @throws IllegalArgumentException * when the propertyName is regarded as being invalid. * * @throws IllegalAccessException * just in case subclasses override this method to try to access * real setter methods and find permission is denied. * * @throws InvocationTargetException * just in case subclasses override this method to try to access * real setter methods, and find it throws an exception when * invoked. * * @throws NoSuchMethodException * just in case subclasses override this method to try to access * real setter methods, and want to fail if no simple method is * available. * @since 1.8.0 */ protected void setPropertyOfMapBean(final Map<String, Object> bean, String propertyName, final Object value) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { if (resolver.isMapped(propertyName)) { final String name = resolver.getProperty(propertyName); if (name == null || name.length() == 0) { propertyName = resolver.getKey(propertyName); } } if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) { throw new IllegalArgumentException( "Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName); } bean.put(propertyName, value); } /** * Set the value of the specified property of the specified bean, no matter * which property reference format is used, with no type conversions. * * @param bean * Bean whose property is to be modified * @param name * Possibly indexed and/or nested name of the property to be * modified * @param value * Value to which this property is to be set * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public void setProperty(final Object bean, final String name, final Object value) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException { setNestedProperty(bean, name, value); } /** * Set the value of the specified simple property of the specified bean, * with no type conversions. * * @param bean * Bean whose property is to be modified * @param name * Name of the property to be modified * @param value * Value to which the property should be set * * @throws IllegalAccessException * if the caller does not have access to the property accessor * method * @throws IllegalArgumentException * if <code>bean</code> or <code>name</code> is null * @throws IllegalArgumentException * if the property name is nested or indexed * @throws InvocationTargetException * if the property accessor method throws an exception * @throws NoSuchMethodException * if an accessor method for this propety cannot be found */ public void setSimpleProperty(final Object bean, final String name, final Object value) { try { _setSimpleProperty(bean, name, value); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public void _setSimpleProperty(final Object bean, final String name, final Object value) throws Exception { if (bean == null) { throw new IllegalArgumentException("No bean specified"); } if (name == null) { throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'"); } // Validate the syntax of the property name if (resolver.hasNested(name)) { throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } else if (resolver.isIndexed(name)) { throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } else if (resolver.isMapped(name)) { throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } if (name.startsWith("@")) { FieldUtils.writeField(bean, name.substring(1), value); return; } // Retrieve the property setter method for the specified property final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); if (descriptor == null) { throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + bean.getClass() + "'"); } final Method writeMethod = getWriteMethod(bean.getClass(), descriptor); if (writeMethod == null) { throw new NoSuchMethodException( "Property '" + name + "' has no setter method in class '" + bean.getClass() + "'"); } // Call the property setter method final Object[] values = new Object[1]; values[0] = value; if (log.isTraceEnabled()) { final String valueClassName = value == null ? "<null>" : value.getClass().getName(); log.trace("setSimpleProperty: Invoking method " + writeMethod + " with value " + value + " (class " + valueClassName + ")"); } invokeMethod(writeMethod, bean, values); } /** This just catches and wraps IllegalArgumentException. */ private Object invokeMethod(final Method method, final Object bean, final Object[] values) throws IllegalAccessException, InvocationTargetException { if (bean == null) { throw new IllegalArgumentException( "No bean specified " + "- this should have been checked before reaching this method"); } try { return method.invoke(bean, values); } catch (final NullPointerException cause) { // JDK 1.3 and JDK 1.4 throw NullPointerException if an argument is // null for a primitive value (JDK 1.5+ throw // IllegalArgumentException) String valueString = ""; if (values != null) { for (int i = 0; i < values.length; i++) { if (i > 0) { valueString += ", "; } if (values[i] == null) { valueString += "<null>"; } else { valueString += values[i].getClass().getName(); } } } String expectedString = ""; final Class<?>[] parTypes = method.getParameterTypes(); if (parTypes != null) { for (int i = 0; i < parTypes.length; i++) { if (i > 0) { expectedString += ", "; } expectedString += parTypes[i].getName(); } } final IllegalArgumentException e = new IllegalArgumentException( "Cannot invoke " + method.getDeclaringClass().getName() + "." + method.getName() + " on bean class '" + bean.getClass() + "' - " + cause.getMessage() // as per // https://issues.apache.org/jira/browse/BEANUTILS-224 + " - had objects of type \"" + valueString + "\" but expected signature \"" + expectedString + "\""); if (!initCause(e, cause)) { log.error("Method invocation failed", cause); } throw e; } catch (final IllegalArgumentException cause) { String valueString = ""; if (values != null) { for (int i = 0; i < values.length; i++) { if (i > 0) { valueString += ", "; } if (values[i] == null) { valueString += "<null>"; } else { valueString += values[i].getClass().getName(); } } } String expectedString = ""; final Class<?>[] parTypes = method.getParameterTypes(); if (parTypes != null) { for (int i = 0; i < parTypes.length; i++) { if (i > 0) { expectedString += ", "; } expectedString += parTypes[i].getName(); } } final IllegalArgumentException e = new IllegalArgumentException( "Cannot invoke " + method.getDeclaringClass().getName() + "." + method.getName() + " on bean class '" + bean.getClass() + "' - " + cause.getMessage() // as per // https://issues.apache.org/jira/browse/BEANUTILS-224 + " - had objects of type \"" + valueString + "\" but expected signature \"" + expectedString + "\""); if (!initCause(e, cause)) { log.error("Method invocation failed", cause); } throw e; } } /** * Obtains the {@code BeanIntrospectionData} object describing the specified * bean class. This object is looked up in the internal cache. If necessary, * introspection is performed now on the affected bean class, and the * results object is created. * * @param beanClass * the bean class in question * @return the {@code BeanIntrospectionData} object for this class * @throws IllegalArgumentException * if the bean class is <b>null</b> */ private BeanIntrospectionData getIntrospectionData(final Class<?> beanClass) { if (beanClass == null) { throw new IllegalArgumentException("No bean class specified"); } // Look up any cached information for this bean class BeanIntrospectionData data = descriptorsCache.get(beanClass); if (data == null) { data = fetchIntrospectionData(beanClass); descriptorsCache.put(beanClass, data); } return data; } /** * Performs introspection on the specified class. This method invokes all * {@code BeanIntrospector} objects that were added to this instance. * * @param beanClass * the class to be inspected * @return a data object with the results of introspection */ private BeanIntrospectionData fetchIntrospectionData(final Class<?> beanClass) { final DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass); for (final BeanIntrospector bi : introspectors) { try { bi.introspect(ictx); } catch (final IntrospectionException iex) { log.error("Exception during introspection", iex); } } return new BeanIntrospectionData(ictx.getPropertyDescriptors()); } /** * Converts an object to a list of objects. This method is used when dealing * with indexed properties. It assumes that indexed properties are stored as * lists of objects. * * @param obj * the object to be converted * @return the resulting list of objects */ private static List<Object> toObjectList(final Object obj) { @SuppressWarnings("unchecked") final // indexed properties are stored in lists of objects List<Object> list = (List<Object>) obj; return list; } /** * Converts an object to a map with property values. This method is used * when dealing with mapped properties. It assumes that mapped properties * are stored in a Map<String, Object>. * * @param obj * the object to be converted * @return the resulting properties map */ private static Map<String, Object> toPropertyMap(final Object obj) { @SuppressWarnings("unchecked") final // mapped properties are stores in maps of type <String, Object> Map<String, Object> map = (Map<String, Object>) obj; return map; } /** * If we're running on JDK 1.4 or later, initialize the cause for the given * throwable. * * @param throwable * The throwable. * @param cause * The cause of the throwable. * @return true if the cause was initialized, otherwise false. * @since 1.8.0 */ public boolean initCause(final Throwable throwable, final Throwable cause) { if (INIT_CAUSE_METHOD != null && cause != null) { try { INIT_CAUSE_METHOD.invoke(throwable, new Object[] { cause }); return true; } catch (final Throwable e) { return false; // can't initialize cause } } return false; } /** * A reference to Throwable's initCause method, or null if it's not there in * this JVM */ private static final Method INIT_CAUSE_METHOD = getInitCauseMethod(); /** * Returns a <code>Method<code> allowing access to * {@link Throwable#initCause(Throwable)} method of {@link Throwable}, * or <code>null</code> if the method does not exist. * * @return A <code>Method<code> for <code>Throwable.initCause</code>, or * <code>null</code> if unavailable. */ private static Method getInitCauseMethod() { try { final Class<?>[] paramsClasses = new Class<?>[] { Throwable.class }; return Throwable.class.getMethod("initCause", paramsClasses); } catch (final NoSuchMethodException e) { final Log log = LogFactory.getLog(QPropertyUtils.class); if (log.isWarnEnabled()) { log.warn("Throwable does not have initCause() method in JDK 1.3"); } return null; } catch (final Throwable e) { final Log log = LogFactory.getLog(QPropertyUtils.class); if (log.isWarnEnabled()) { log.warn("Error getting the Throwable initCause() method", e); } return null; } } }