    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.beanutils;
    019
    020
    021import java.beans.IndexedPropertyDescriptor;
    022import java.beans.IntrospectionException;
    023import java.beans.Introspector;
    024import java.beans.PropertyDescriptor;
    025import java.lang.reflect.Array;
    026import java.lang.reflect.InvocationTargetException;
    027import java.lang.reflect.Method;
    028import java.util.HashMap;
    029import java.util.Iterator;
    030import java.util.List;
    031import java.util.Map;
    032import java.util.Map.Entry;
    033import java.util.concurrent.CopyOnWriteArrayList;
    034
    035import org.apache.commons.beanutils.expression.DefaultResolver;
    036import org.apache.commons.beanutils.expression.Resolver;
    037import org.apache.commons.collections.FastHashMap;
    038import org.apache.commons.logging.Log;
    039import org.apache.commons.logging.LogFactory;
    040
    041
    042/**
    043 * Utility methods for using Java Reflection APIs to facilitate generic
    044 * property getter and setter operations on Java objects.  Much of this
    045 * code was originally included in <code>BeanUtils</code>, but has been
    046 * separated because of the volume of code involved.
    047 * <p>
    048 * In general, the objects that are examined and modified using these
    049 * methods are expected to conform to the property getter and setter method
    050 * naming conventions described in the JavaBeans Specification (Version 1.0.1).
    051 * No data type conversions are performed, and there are no usage of any
    052 * <code>PropertyEditor</code> classes that have been registered, although
    053 * a convenient way to access the registered classes themselves is included.
    054 * <p>
    055 * For the purposes of this class, five formats for referencing a particular
    056 * property value of a bean are defined, with the <i>default</i> layout of an
    057 * identifying String in parentheses. However the notation for these formats
    058 * and how they are resolved is now (since BeanUtils 1.8.0) controlled by
    059 * the configured {@link Resolver} implementation:
    060 * <ul>
    061 * <li><strong>Simple (<code>name</code>)</strong> - The specified
    062 *     <code>name</code> identifies an individual property of a particular
    063 *     JavaBean.  The name of the actual getter or setter method to be used
    064 *     is determined using standard JavaBeans instrospection, so that (unless
    065 *     overridden by a <code>BeanInfo</code> class, a property named "xyz"
    066 *     will have a getter method named <code>getXyz()</code> or (for boolean
    067 *     properties only) <code>isXyz()</code>, and a setter method named
    068 *     <code>setXyz()</code>.</li>
    069 * <li><strong>Nested (<code>name1.name2.name3</code>)</strong> The first
    070 *     name element is used to select a property getter, as for simple
    071 *     references above.  The object returned for this property is then
    072 *     consulted, using the same approach, for a property getter for a
    073 *     property named <code>name2</code>, and so on.  The property value that
    074 *     is ultimately retrieved or modified is the one identified by the
    075 *     last name element.</li>
    076 * <li><strong>Indexed (<code>name[index]</code>)</strong> - The underlying
    077 *     property value is assumed to be an array, or this JavaBean is assumed
    078 *     to have indexed property getter and setter methods.  The appropriate
    079 *     (zero-relative) entry in the array is selected.  <code>List</code>
    080 *     objects are now also supported for read/write.  You simply need to define
    081 *     a getter that returns the <code>List</code></li>
    082 * <li><strong>Mapped (<code>name(key)</code>)</strong> - The JavaBean
    083 *     is assumed to have an property getter and setter methods with an
    084 *     additional attribute of type <code>java.lang.String</code>.</li>
    085 * <li><strong>Combined (<code>name1.name2[index].name3(key)</code>)</strong> -
    086 *     Combining mapped, nested, and indexed references is also
    087 *     supported.</li>
    088 * </ul>
    089 *
    090 * @version $Id$
    091 * @see Resolver
    092 * @see PropertyUtils
    093 * @since 1.7
    094 */
    095
    096public class PropertyUtilsBean {
    097
    098    private Resolver resolver = new DefaultResolver();
    099
    100    // --------------------------------------------------------- Class Methods
    101
    102    /**
    103     * Return the PropertyUtils bean instance.
    104     * @return The PropertyUtils bean instance
    105     */
    106    protected static PropertyUtilsBean getInstance() {
    107        return BeanUtilsBean.getInstance().getPropertyUtils();
    108    }
    109
    110    // --------------------------------------------------------- Variables
    111
    112    /**
    113     * The cache of PropertyDescriptor arrays for beans we have already
    114     * introspected, keyed by the java.lang.Class of this object.
    115     */
    116    private WeakFastHashMap<Class<?>, BeanIntrospectionData> descriptorsCache = null;
    117    private WeakFastHashMap<Class<?>, FastHashMap> mappedDescriptorsCache = null;
    118
    119    /** An empty object array */
    120    private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
    121
    122    /** Log instance */
    123    private final Log log = LogFactory.getLog(PropertyUtils.class);
    124
    125    /** The list with BeanIntrospector objects. */
    126    private final List<BeanIntrospector> introspectors;
    127
    128    // ---------------------------------------------------------- Constructors
    129
    130    /** Base constructor */
    131    public PropertyUtilsBean() {
    132        descriptorsCache = new WeakFastHashMap<Class<?>, BeanIntrospectionData>();
    133        descriptorsCache.setFast(true);
    134        mappedDescriptorsCache = new WeakFastHashMap<Class<?>, FastHashMap>();
    135        mappedDescriptorsCache.setFast(true);
    136        introspectors = new CopyOnWriteArrayList<BeanIntrospector>();
    137        resetBeanIntrospectors();
    138    }
    139
    140
    141    // --------------------------------------------------------- Public Methods
    142
    143
    144    /**
    145     * Return the configured {@link Resolver} implementation used by BeanUtils.
    146     * <p>
    147     * The {@link Resolver} handles the <i>property name</i>
    148     * expressions and the implementation in use effectively
    149     * controls the dialect of the <i>expression language</i>
    150     * that BeanUtils recongnises.
    151     * <p>
    152     * {@link DefaultResolver} is the default implementation used.
    153     *
    154     * @return resolver The property expression resolver.
    155     * @since 1.8.0
    156     */
    157    public Resolver getResolver() {
    158        return resolver;
    159    }
    160
    161    /**
    162     * Configure the {@link Resolver} implementation used by BeanUtils.
    163     * <p>
    164     * The {@link Resolver} handles the <i>property name</i>
    165     * expressions and the implementation in use effectively
    166     * controls the dialect of the <i>expression language</i>
    167     * that BeanUtils recongnises.
    168     * <p>
    169     * {@link DefaultResolver} is the default implementation used.
    170     *
    171     * @param resolver The property expression resolver.
    172     * @since 1.8.0
    173     */
    174    public void setResolver(final Resolver resolver) {
    175        if (resolver == null) {
    176            this.resolver = new DefaultResolver();
    177        } else {
    178            this.resolver = resolver;
    179        }
    180    }
    181
    182    /**
    183     * Resets the {@link BeanIntrospector} objects registered at this instance. After this
    184     * method was called, only the default {@code BeanIntrospector} is registered.
    185     *
    186     * @since 1.9
    187     */
    188    public final void resetBeanIntrospectors() {
    189        introspectors.clear();
    190        introspectors.add(DefaultBeanIntrospector.INSTANCE);
    191        introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS);
    192    }
    193
    194    /**
    195     * Adds a <code>BeanIntrospector</code>. This object is invoked when the
    196     * property descriptors of a class need to be obtained.
    197     *
    198     * @param introspector the <code>BeanIntrospector</code> to be added (must
    199     *        not be <b>null</b>
    200     * @throws IllegalArgumentException if the argument is <b>null</b>
    201     * @since 1.9
    202     */
    203    public void addBeanIntrospector(final BeanIntrospector introspector) {
    204        if (introspector == null) {
    205            throw new IllegalArgumentException(
    206                    "BeanIntrospector must not be null!");
    207        }
    208        introspectors.add(introspector);
    209    }
    210
    211    /**
    212     * Removes the specified <code>BeanIntrospector</code>.
    213     *
    214     * @param introspector the <code>BeanIntrospector</code> to be removed
    215     * @return <b>true</b> if the <code>BeanIntrospector</code> existed and
    216     *         could be removed, <b>false</b> otherwise
    217     * @since 1.9
    218     */
    219    public boolean removeBeanIntrospector(final BeanIntrospector introspector) {
    220        return introspectors.remove(introspector);
    221    }
    222
    223    /**
    224     * Clear any cached property descriptors information for all classes
    225     * loaded by any class loaders.  This is useful in cases where class
    226     * loaders are thrown away to implement class reloading.
    227     */
    228    public void clearDescriptors() {
    229
    230        descriptorsCache.clear();
    231        mappedDescriptorsCache.clear();
    232        Introspector.flushCaches();
    233
    234    }
    235
    236
    237    /**
    238     * <p>Copy property values from the "origin" bean to the "destination" bean
    239     * for all cases where the property names are the same (even though the
    240     * actual getter and setter methods might have been customized via
    241     * <code>BeanInfo</code> classes).  No conversions are performed on the
    242     * actual property values -- it is assumed that the values retrieved from
    243     * the origin bean are assignment-compatible with the types expected by
    244     * the destination bean.</p>
    245     *
    246     * <p>If the origin "bean" is actually a <code>Map</code>, it is assumed
    247     * to contain String-valued <strong>simple</strong> property names as the keys, pointing
    248     * at the corresponding property values that will be set in the destination
    249     * bean.<strong>Note</strong> that this method is intended to perform
    250     * a "shallow copy" of the properties and so complex properties
    251     * (for example, nested ones) will not be copied.</p>
    252     *
    253     * <p>Note, that this method will not copy a List to a List, or an Object[]
    254     * to an Object[]. It's specifically for copying JavaBean properties. </p>
    255     *
    256     * @param dest Destination bean whose properties are modified
    257     * @param orig Origin bean whose properties are retrieved
    258     *
    259     * @throws IllegalAccessException if the caller does not have
    260     *  access to the property accessor method
    261     * @throws IllegalArgumentException if the <code>dest</code> or
    262     *  <code>orig</code> argument is null
    263     * @throws InvocationTargetException if the property accessor method
    264     *  throws an exception
    265     * @throws NoSuchMethodException if an accessor method for this
    266     *  propety cannot be found
    267     */
    268    public void copyProperties(final Object dest, final Object orig)
    269            throws IllegalAccessException, InvocationTargetException,
    270            NoSuchMethodException {
    271
    272        if (dest == null) {
    273            throw new IllegalArgumentException
    274                    ("No destination bean specified");
    275        }
    276        if (orig == null) {
    277            throw new IllegalArgumentException("No origin bean specified");
    278        }
    279
    280        if (orig instanceof DynaBean) {
    281            final DynaProperty[] origDescriptors =
    282                ((DynaBean) orig).getDynaClass().getDynaProperties();
    283            for (DynaProperty origDescriptor : origDescriptors) {
    284                final String name = origDescriptor.getName();
    285                if (isReadable(orig, name) && isWriteable(dest, name)) {
    286                    try {
    287                        final Object value = ((DynaBean) orig).get(name);
    288                        if (dest instanceof DynaBean) {
    289                            ((DynaBean) dest).set(name, value);
    290                        } else {
    291                                setSimpleProperty(dest, name, value);
    292                        }
    293                    } catch (final NoSuchMethodException e) {
    294                        if (log.isDebugEnabled()) {
    295                            log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
    296                        }
    297                    }
    298                }
    299            }
    300        } else if (orig instanceof Map) {
    301            final Iterator<?> entries = ((Map<?, ?>) orig).entrySet().iterator();
    302            while (entries.hasNext()) {
    303                final Map.Entry<?, ?> entry = (Entry<?, ?>) entries.next();
    304                final String name = (String)entry.getKey();
    305                if (isWriteable(dest, name)) {
    306                    try {
    307                        if (dest instanceof DynaBean) {
    308                            ((DynaBean) dest).set(name, entry.getValue());
    309                        } else {
    310                            setSimpleProperty(dest, name, entry.getValue());
    311                        }
    312                    } catch (final NoSuchMethodException e) {
    313                        if (log.isDebugEnabled()) {
    314                            log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
    315                        }
    316                    }
    317                }
    318            }
    319        } else /* if (orig is a standard JavaBean) */ {
    320            final PropertyDescriptor[] origDescriptors =
    321                getPropertyDescriptors(orig);
    322            for (PropertyDescriptor origDescriptor : origDescriptors) {
    323                final String name = origDescriptor.getName();
    324                if (isReadable(orig, name) && isWriteable(dest, name)) {
    325                    try {
    326                        final Object value = getSimpleProperty(orig, name);
    327                        if (dest instanceof DynaBean) {
    328                            ((DynaBean) dest).set(name, value);
    329                        } else {
    330                                setSimpleProperty(dest, name, value);
    331                        }
    332                    } catch (final NoSuchMethodException e) {
    333                        if (log.isDebugEnabled()) {
    334                            log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
    335                        }
    336                    }
    337                }
    338            }
    339        }
    340
    341    }
    342
    343
    344    /**
    345     * <p>Return the entire set of properties for which the specified bean
    346     * provides a read method.  This map contains the unconverted property
    347     * values for all properties for which a read method is provided
    348     * (i.e. where the <code>getReadMethod()</code> returns non-null).</p>
    349     *
    350     * <p><strong>FIXME</strong> - Does not account for mapped properties.</p>
    351     *
    352     * @param bean Bean whose properties are to be extracted
    353     * @return The set of properties for the bean
    354     *
    355     * @throws IllegalAccessException if the caller does not have
    356     *  access to the property accessor method
    357     * @throws IllegalArgumentException if <code>bean</code> is null
    358     * @throws InvocationTargetException if the property accessor method
    359     *  throws an exception
    360     * @throws NoSuchMethodException if an accessor method for this
    361     *  propety cannot be found
    362     */
    363    public Map<String, Object> describe(final Object bean)
    364            throws IllegalAccessException, InvocationTargetException,
    365            NoSuchMethodException {
    366
    367        if (bean == null) {
    368            throw new IllegalArgumentException("No bean specified");
    369        }
    370        final Map<String, Object> description = new HashMap<String, Object>();
    371        if (bean instanceof DynaBean) {
    372            final DynaProperty[] descriptors =
    373                ((DynaBean) bean).getDynaClass().getDynaProperties();
    374            for (DynaProperty descriptor : descriptors) {
    375                final String name = descriptor.getName();
    376                description.put(name, getProperty(bean, name));
    377            }
    378        } else {
    379            final PropertyDescriptor[] descriptors =
    380                getPropertyDescriptors(bean);
    381            for (PropertyDescriptor descriptor : descriptors) {
    382                final String name = descriptor.getName();
    383                if (descriptor.getReadMethod() != null) {
    384                    description.put(name, getProperty(bean, name));
    385                }
    386            }
    387        }
    388        return (description);
    389
    390    }
    391
    392
    393    /**
    394     * Return the value of the specified indexed property of the specified
    395     * bean, with no type conversions.  The zero-relative index of the
    396     * required value must be included (in square brackets) as a suffix to
    397     * the property name, or <code>IllegalArgumentException</code> will be
    398     * thrown.  In addition to supporting the JavaBeans specification, this
    399     * method has been extended to support <code>List</code> objects as well.
    400     *
    401     * @param bean Bean whose property is to be extracted
    402     * @param name <code>propertyname[index]</code> of the property value
    403     *  to be extracted
    404     * @return the indexed property value
    405     *
    406     * @throws IndexOutOfBoundsException if the specified index
    407     *  is outside the valid range for the underlying array or List
    408     * @throws IllegalAccessException if the caller does not have
    409     *  access to the property accessor method
    410     * @throws IllegalArgumentException if <code>bean</code> or
    411     *  <code>name</code> is null
    412     * @throws InvocationTargetException if the property accessor method
    413     *  throws an exception
    414     * @throws NoSuchMethodException if an accessor method for this
    415     *  propety cannot be found
    416     */
    417    public Object getIndexedProperty(final Object bean, String name)
    418            throws IllegalAccessException, InvocationTargetException,
    419            NoSuchMethodException {
    420
    421        if (bean == null) {
    422            throw new IllegalArgumentException("No bean specified");
    423        }
    424        if (name == null) {
    425            throw new IllegalArgumentException("No name specified for bean class '" +
    426                    bean.getClass() + "'");
    427        }
    428
    429        // Identify the index of the requested individual property
    430        int index = -1;
    431        try {
    432            index = resolver.getIndex(name);
    433        } catch (final IllegalArgumentException e) {
    434            throw new IllegalArgumentException("Invalid indexed property '" +
    435                    name + "' on bean class '" + bean.getClass() + "' " +
    436                    e.getMessage());
    437        }
    438        if (index < 0) {
    439            throw new IllegalArgumentException("Invalid indexed property '" +
    440                    name + "' on bean class '" + bean.getClass() + "'");
    441        }
    442
    443        // Isolate the name
    444        name = resolver.getProperty(name);
    445
    446        // Request the specified indexed property value
    447        return (getIndexedProperty(bean, name, index));
    448
    449    }
    450
    451
    452    /**
    453     * Return the value of the specified indexed property of the specified
    454     * bean, with no type conversions.  In addition to supporting the JavaBeans
    455     * specification, this method has been extended to support
    456     * <code>List</code> objects as well.
    457     *
    458     * @param bean Bean whose property is to be extracted
    459     * @param name Simple property name of the property value to be extracted
    460     * @param index Index of the property value to be extracted
    461     * @return the indexed property value
    462     *
    463     * @throws IndexOutOfBoundsException if the specified index
    464     *  is outside the valid range for the underlying property
    465     * @throws IllegalAccessException if the caller does not have
    466     *  access to the property accessor method
    467     * @throws IllegalArgumentException if <code>bean</code> or
    468     *  <code>name</code> is null
    469     * @throws InvocationTargetException if the property accessor method
    470     *  throws an exception
    471     * @throws NoSuchMethodException if an accessor method for this
    472     *  propety cannot be found
    473     */
    474    public Object getIndexedProperty(final Object bean,
    475                                            final String name, final int index)
    476            throws IllegalAccessException, InvocationTargetException,
    477            NoSuchMethodException {
    478
    479        if (bean == null) {
    480            throw new IllegalArgumentException("No bean specified");
    481        }
    482        if (name == null || name.length() == 0) {
    483            if (bean.getClass().isArray()) {
    484                return Array.get(bean, index);
    485            } else if (bean instanceof List) {
    486                return ((List<?>)bean).get(index);
    487            }
    488        }
    489        if (name == null) {
    490            throw new IllegalArgumentException("No name specified for bean class '" +
    491                    bean.getClass() + "'");
    492        }
    493
    494        // Handle DynaBean instances specially
    495        if (bean instanceof DynaBean) {
    496            final DynaProperty descriptor =
    497                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
    498            if (descriptor == null) {
    499                throw new NoSuchMethodException("Unknown property '" +
    500                    name + "' on bean class '" + bean.getClass() + "'");
    501            }
    502            return (((DynaBean) bean).get(name, index));
    503        }
    504
    505        // Retrieve the property descriptor for the specified property
    506        final PropertyDescriptor descriptor =
    507                getPropertyDescriptor(bean, name);
    508        if (descriptor == null) {
    509            throw new NoSuchMethodException("Unknown property '" +
    510                    name + "' on bean class '" + bean.getClass() + "'");
    511        }
    512
    513        // Call the indexed getter method if there is one
    514        if (descriptor instanceof IndexedPropertyDescriptor) {
    515            Method readMethod = ((IndexedPropertyDescriptor) descriptor).
    516                    getIndexedReadMethod();
    517            readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
    518            if (readMethod != null) {
    519                final Object[] subscript = new Object[1];
    520                subscript[0] = new Integer(index);
    521                try {
    522                    return (invokeMethod(readMethod,bean, subscript));
    523                } catch (final InvocationTargetException e) {
    524                    if (e.getTargetException() instanceof
    525                            IndexOutOfBoundsException) {
    526                        throw (IndexOutOfBoundsException)
    527                                e.getTargetException();
    528                    } else {
    529                        throw e;
    530                    }
    531                }
    532            }
    533        }
    534
    535        // Otherwise, the underlying property must be an array
    536        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
    537        if (readMethod == null) {
    538            throw new NoSuchMethodException("Property '" + name + "' has no " +
    539                    "getter method on bean class '" + bean.getClass() + "'");
    540        }
    541
    542        // Call the property getter and return the value
    543        final Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
    544        if (!value.getClass().isArray()) {
    545            if (!(value instanceof java.util.List)) {
    546                throw new IllegalArgumentException("Property '" + name +
    547                        "' is not indexed on bean class '" + bean.getClass() + "'");
    548            } else {
    549                //get the List's value
    550                return ((java.util.List<?>) value).get(index);
    551            }
    552        } else {
    553            //get the array's value
    554            try {
    555                return (Array.get(value, index));
    556            } catch (final ArrayIndexOutOfBoundsException e) {
    557                throw new ArrayIndexOutOfBoundsException("Index: " +
    558                        index + ", Size: " + Array.getLength(value) +
    559                        " for property '" + name + "'");
    560            }
    561        }
    562
    563    }
    564
    565
    566    /**
    567     * Return the value of the specified mapped property of the
    568     * specified bean, with no type conversions.  The key of the
    569     * required value must be included (in brackets) as a suffix to
    570     * the property name, or <code>IllegalArgumentException</code> will be
    571     * thrown.
    572     *
    573     * @param bean Bean whose property is to be extracted
    574     * @param name <code>propertyname(key)</code> of the property value
    575     *  to be extracted
    576     * @return the mapped property value
    577     *
    578     * @throws IllegalAccessException if the caller does not have
    579     *  access to the property accessor method
    580     * @throws InvocationTargetException if the property accessor method
    581     *  throws an exception
    582     * @throws NoSuchMethodException if an accessor method for this
    583     *  propety cannot be found
    584     */
    585    public Object getMappedProperty(final Object bean, String name)
    586            throws IllegalAccessException, InvocationTargetException,
    587            NoSuchMethodException {
    588
    589        if (bean == null) {
    590            throw new IllegalArgumentException("No bean specified");
    591        }
    592        if (name == null) {
    593            throw new IllegalArgumentException("No name specified for bean class '" +
    594                    bean.getClass() + "'");
    595        }
    596
    597        // Identify the key of the requested individual property
    598        String key  = null;
    599        try {
    600            key = resolver.getKey(name);
    601        } catch (final IllegalArgumentException e) {
    602            throw new IllegalArgumentException
    603                    ("Invalid mapped property '" + name +
    604                    "' on bean class '" + bean.getClass() + "' " + e.getMessage());
    605        }
    606        if (key == null) {
    607            throw new IllegalArgumentException("Invalid mapped property '" +
    608                    name + "' on bean class '" + bean.getClass() + "'");
    609        }
    610
    611        // Isolate the name
    612        name = resolver.getProperty(name);
    613
    614        // Request the specified indexed property value
    615        return (getMappedProperty(bean, name, key));
    616
    617    }
    618
    619
    620    /**
    621     * Return the value of the specified mapped property of the specified
    622     * bean, with no type conversions.
    623     *
    624     * @param bean Bean whose property is to be extracted
    625     * @param name Mapped property name of the property value to be extracted
    626     * @param key Key of the property value to be extracted
    627     * @return the mapped property value
    628     *
    629     * @throws IllegalAccessException if the caller does not have
    630     *  access to the property accessor method
    631     * @throws InvocationTargetException if the property accessor method
    632     *  throws an exception
    633     * @throws NoSuchMethodException if an accessor method for this
    634     *  propety cannot be found
    635     */
    636    public Object getMappedProperty(final Object bean,
    637                                           final String name, final String key)
    638            throws IllegalAccessException, InvocationTargetException,
    639            NoSuchMethodException {
    640
    641        if (bean == null) {
    642            throw new IllegalArgumentException("No bean specified");
    643        }
    644        if (name == null) {
    645            throw new IllegalArgumentException("No name specified for bean class '" +
    646                    bean.getClass() + "'");
    647        }
    648        if (key == null) {
    649            throw new IllegalArgumentException("No key specified for property '" +
    650                    name + "' on bean class " + bean.getClass() + "'");
    651        }
    652
    653        // Handle DynaBean instances specially
    654        if (bean instanceof DynaBean) {
    655            final DynaProperty descriptor =
    656                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
    657            if (descriptor == null) {
    658                throw new NoSuchMethodException("Unknown property '" +
    659                        name + "'+ on bean class '" + bean.getClass() + "'");
    660            }
    661            return (((DynaBean) bean).get(name, key));
    662        }
    663
    664        Object result = null;
    665
    666        // Retrieve the property descriptor for the specified property
    667        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
    668        if (descriptor == null) {
    669            throw new NoSuchMethodException("Unknown property '" +
    670                    name + "'+ on bean class '" + bean.getClass() + "'");
    671        }
    672
    673        if (descriptor instanceof MappedPropertyDescriptor) {
    674            // Call the keyed getter method if there is one
    675            Method readMethod = ((MappedPropertyDescriptor) descriptor).
    676                    getMappedReadMethod();
    677            readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
    678            if (readMethod != null) {
    679                final Object[] keyArray = new Object[1];
    680                keyArray[0] = key;
    681                result = invokeMethod(readMethod, bean, keyArray);
    682            } else {
    683                throw new NoSuchMethodException("Property '" + name +
    684                        "' has no mapped getter method on bean class '" +
    685                        bean.getClass() + "'");
    686            }
    687        } else {
    688          /* means that the result has to be retrieved from a map */
    689          final Method readMethod = getReadMethod(bean.getClass(), descriptor);
    690          if (readMethod != null) {
    691            final Object invokeResult = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
    692            /* test and fetch from the map */
    693            if (invokeResult instanceof java.util.Map) {
    694              result = ((java.util.Map<?, ?>)invokeResult).get(key);
    695            }
    696          } else {
    697            throw new NoSuchMethodException("Property '" + name +
    698                    "' has no mapped getter method on bean class '" +
    699                    bean.getClass() + "'");
    700          }
    701        }
    702        return result;
    703
    704    }
    705
    706
    707    /**
    708     * <p>Return the mapped property descriptors for this bean class.</p>
    709     *
    710     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    711     *
    712     * @param beanClass Bean class to be introspected
    713     * @return the mapped property descriptors
    714     * @deprecated This method should not be exposed
    715     */
    716    @Deprecated
    717    public FastHashMap getMappedPropertyDescriptors(final Class<?> beanClass) {
    718
    719        if (beanClass == null) {
    720            return null;
    721        }
    722
    723        // Look up any cached descriptors for this bean class
    724        return mappedDescriptorsCache.get(beanClass);
    725
    726    }
    727
    728
    729    /**
    730     * <p>Return the mapped property descriptors for this bean.</p>
    731     *
    732     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    733     *
    734     * @param bean Bean to be introspected
    735     * @return the mapped property descriptors
    736     * @deprecated This method should not be exposed
    737     */
    738    @Deprecated
    739    public FastHashMap getMappedPropertyDescriptors(final Object bean) {
    740
    741        if (bean == null) {
    742            return null;
    743        }
    744        return (getMappedPropertyDescriptors(bean.getClass()));
    745
    746    }
    747
    748
    749    /**
    750     * Return the value of the (possibly nested) property of the specified
    751     * name, for the specified bean, with no type conversions.
    752     *
    753     * @param bean Bean whose property is to be extracted
    754     * @param name Possibly nested name of the property to be extracted
    755     * @return the nested property value
    756     *
    757     * @throws IllegalAccessException if the caller does not have
    758     *  access to the property accessor method
    759     * @throws IllegalArgumentException if <code>bean</code> or
    760     *  <code>name</code> is null
    761     * @throws NestedNullException if a nested reference to a
    762     *  property returns null
    763     * @throws InvocationTargetException
    764     * if the property accessor method throws an exception
    765     * @throws NoSuchMethodException if an accessor method for this
    766     *  propety cannot be found
    767     */
    768    public Object getNestedProperty(Object bean, String name)
    769            throws IllegalAccessException, InvocationTargetException,
    770            NoSuchMethodException {
    771
    772        if (bean == null) {
    773            throw new IllegalArgumentException("No bean specified");
    774        }
    775        if (name == null) {
    776            throw new IllegalArgumentException("No name specified for bean class '" +
    777                    bean.getClass() + "'");
    778        }
    779
    780        // Resolve nested references
    781        while (resolver.hasNested(name)) {
    782            final String next = resolver.next(name);
    783            Object nestedBean = null;
    784            if (bean instanceof Map) {
    785                nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next);
    786            } else if (resolver.isMapped(next)) {
    787                nestedBean = getMappedProperty(bean, next);
    788            } else if (resolver.isIndexed(next)) {
    789                nestedBean = getIndexedProperty(bean, next);
    790            } else {
    791                nestedBean = getSimpleProperty(bean, next);
    792            }
    793            if (nestedBean == null) {
    794                throw new NestedNullException
    795                        ("Null property value for '" + name +
    796                        "' on bean class '" + bean.getClass() + "'");
    797            }
    798            bean = nestedBean;
    799            name = resolver.remove(name);
    800        }
    801
    802        if (bean instanceof Map) {
    803            bean = getPropertyOfMapBean((Map<?, ?>) bean, name);
    804        } else if (resolver.isMapped(name)) {
    805            bean = getMappedProperty(bean, name);
    806        } else if (resolver.isIndexed(name)) {
    807            bean = getIndexedProperty(bean, name);
    808        } else {
    809            bean = getSimpleProperty(bean, name);
    810        }
    811        return bean;
    812
    813    }
    814
    815    /**
    816     * This method is called by getNestedProperty and setNestedProperty to
    817     * define what it means to get a property from an object which implements
    818     * Map. See setPropertyOfMapBean for more information.
    819     *
    820     * @param bean Map bean
    821     * @param propertyName The property name
    822     * @return the property value
    823     *
    824     * @throws IllegalArgumentException when the propertyName is regarded as
    825     * being invalid.
    826     *
    827     * @throws IllegalAccessException just in case subclasses override this
    828     * method to try to access real getter methods and find permission is denied.
    829     *
    830     * @throws InvocationTargetException just in case subclasses override this
    831     * method to try to access real getter methods, and find it throws an
    832     * exception when invoked.
    833     *
    834     * @throws NoSuchMethodException just in case subclasses override this
    835     * method to try to access real getter methods, and want to fail if
    836     * no simple method is available.
    837     * @since 1.8.0
    838     */
    839    protected Object getPropertyOfMapBean(final Map<?, ?> bean, String propertyName)
    840        throws IllegalArgumentException, IllegalAccessException,
    841        InvocationTargetException, NoSuchMethodException {
    842
    843        if (resolver.isMapped(propertyName)) {
    844            final String name = resolver.getProperty(propertyName);
    845            if (name == null || name.length() == 0) {
    846                propertyName = resolver.getKey(propertyName);
    847            }
    848        }
    849
    850        if (resolver.isIndexed(propertyName) ||
    851            resolver.isMapped(propertyName)) {
    852            throw new IllegalArgumentException(
    853                    "Indexed or mapped properties are not supported on"
    854                    + " objects of type Map: " + propertyName);
    855        }
    856
    857        return bean.get(propertyName);
    858    }
    859
    860
    861
    862    /**
    863     * Return the value of the specified property of the specified bean,
    864     * no matter which property reference format is used, with no
    865     * type conversions.
    866     *
    867     * @param bean Bean whose property is to be extracted
    868     * @param name Possibly indexed and/or nested name of the property
    869     *  to be extracted
    870     * @return the property value
    871     *
    872     * @throws IllegalAccessException if the caller does not have
    873     *  access to the property accessor method
    874     * @throws IllegalArgumentException if <code>bean</code> or
    875     *  <code>name</code> is null
    876     * @throws InvocationTargetException if the property accessor method
    877     *  throws an exception
    878     * @throws NoSuchMethodException if an accessor method for this
    879     *  propety cannot be found
    880     */
    881    public Object getProperty(final Object bean, final String name)
    882            throws IllegalAccessException, InvocationTargetException,
    883            NoSuchMethodException {
    884
    885        return (getNestedProperty(bean, name));
    886
    887    }
    888
    889
    890    /**
    891     * <p>Retrieve the property descriptor for the specified property of the
    892     * specified bean, or return <code>null</code> if there is no such
    893     * descriptor.  This method resolves indexed and nested property
    894     * references in the same manner as other methods in this class, except
    895     * that if the last (or only) name element is indexed, the descriptor
    896     * for the last resolved property itself is returned.</p>
    897     *
    898     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    899     *
    900     * <p>Note that for Java 8 and above, this method no longer return
    901     * IndexedPropertyDescriptor for {@link List}-typed properties, only for
    902     * properties typed as native array. (BEANUTILS-492).
    903     *
    904     * @param bean Bean for which a property descriptor is requested
    905     * @param name Possibly indexed and/or nested name of the property for
    906     *  which a property descriptor is requested
    907     * @return the property descriptor
    908     *
    909     * @throws IllegalAccessException if the caller does not have
    910     *  access to the property accessor method
    911     * @throws IllegalArgumentException if <code>bean</code> or
    912     *  <code>name</code> is null
    913     * @throws IllegalArgumentException if a nested reference to a
    914     *  property returns null
    915     * @throws InvocationTargetException if the property accessor method
    916     *  throws an exception
    917     * @throws NoSuchMethodException if an accessor method for this
    918     *  propety cannot be found
    919     */
    920    public PropertyDescriptor getPropertyDescriptor(Object bean,
    921                                                           String name)
    922            throws IllegalAccessException, InvocationTargetException,
    923            NoSuchMethodException {
    924
    925        if (bean == null) {
    926            throw new IllegalArgumentException("No bean specified");
    927        }
    928        if (name == null) {
    929            throw new IllegalArgumentException("No name specified for bean class '" +
    930                    bean.getClass() + "'");
    931        }
    932
    933        // Resolve nested references
    934        while (resolver.hasNested(name)) {
    935            final String next = resolver.next(name);
    936            final Object nestedBean = getProperty(bean, next);
    937            if (nestedBean == null) {
    938                throw new NestedNullException
    939                        ("Null property value for '" + next +
    940                        "' on bean class '" + bean.getClass() + "'");
    941            }
    942            bean = nestedBean;
    943            name = resolver.remove(name);
    944        }
    945
    946        // Remove any subscript from the final name value
    947        name = resolver.getProperty(name);
    948
    949        // Look up and return this property from our cache
    950        // creating and adding it to the cache if not found.
    951        if (name == null) {
    952            return (null);
    953        }
    954
    955        final BeanIntrospectionData data = getIntrospectionData(bean.getClass());
    956        PropertyDescriptor result = data.getDescriptor(name);
    957        if (result != null) {
    958            return result;
    959        }
    960
    961        FastHashMap mappedDescriptors =
    962                getMappedPropertyDescriptors(bean);
    963        if (mappedDescriptors == null) {
    964            mappedDescriptors = new FastHashMap();
    965            mappedDescriptors.setFast(true);
    966            mappedDescriptorsCache.put(bean.getClass(), mappedDescriptors);
    967        }
    968        result = (PropertyDescriptor) mappedDescriptors.get(name);
    969        if (result == null) {
    970            // not found, try to create it
    971            try {
    972                result = new MappedPropertyDescriptor(name, bean.getClass());
    973            } catch (final IntrospectionException ie) {
    974                /* Swallow IntrospectionException
    975                 * TODO: Why?
    976                 */
    977            }
    978            if (result != null) {
    979                mappedDescriptors.put(name, result);
    980            }
    981        }
    982
    983        return result;
    984
    985    }
    986
    987
    988    /**
    989     * <p>Retrieve the property descriptors for the specified class,
    990     * introspecting and caching them the first time a particular bean class
    991     * is encountered.</p>
    992     *
    993     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    994     *
    995     * @param beanClass Bean class for which property descriptors are requested
    996     * @return the property descriptors
    997     *
    998     * @throws IllegalArgumentException if <code>beanClass</code> is null
    999     */
    1000    public PropertyDescriptor[]
    1001            getPropertyDescriptors(final Class<?> beanClass) {
    1002
    1003        return getIntrospectionData(beanClass).getDescriptors();
    1004
    1005    }
    1006
    1007    /**
    1008     * <p>Retrieve the property descriptors for the specified bean,
    1009     * introspecting and caching them the first time a particular bean class
    1010     * is encountered.</p>
    1011     *
    1012     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    1013     *
    1014     * @param bean Bean for which property descriptors are requested
    1015     * @return the property descriptors
    1016     *
    1017     * @throws IllegalArgumentException if <code>bean</code> is null
    1018     */
    1019    public PropertyDescriptor[] getPropertyDescriptors(final Object bean) {
    1020
    1021        if (bean == null) {
    1022            throw new IllegalArgumentException("No bean specified");
    1023        }
    1024        return (getPropertyDescriptors(bean.getClass()));
    1025
    1026    }
    1027
    1028
    1029    /**
    1030     * <p>Return the Java Class repesenting the property editor class that has
    1031     * been registered for this property (if any).  This method follows the
    1032     * same name resolution rules used by <code>getPropertyDescriptor()</code>,
    1033     * so if the last element of a name reference is indexed, the property
    1034     * editor for the underlying property's class is returned.</p>
    1035     *
    1036     * <p>Note that <code>null</code> will be returned if there is no property,
    1037     * or if there is no registered property editor class.  Because this
    1038     * return value is ambiguous, you should determine the existence of the
    1039     * property itself by other means.</p>
    1040     *
    1041     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    1042     *
    1043     * @param bean Bean for which a property descriptor is requested
    1044     * @param name Possibly indexed and/or nested name of the property for
    1045     *  which a property descriptor is requested
    1046     * @return the property editor class
    1047     *
    1048     * @throws IllegalAccessException if the caller does not have
    1049     *  access to the property accessor method
    1050     * @throws IllegalArgumentException if <code>bean</code> or
    1051     *  <code>name</code> is null
    1052     * @throws IllegalArgumentException if a nested reference to a
    1053     *  property returns null
    1054     * @throws InvocationTargetException if the property accessor method
    1055     *  throws an exception
    1056     * @throws NoSuchMethodException if an accessor method for this
    1057     *  propety cannot be found
    1058     */
    1059    public Class<?> getPropertyEditorClass(final Object bean, final String name)
    1060            throws IllegalAccessException, InvocationTargetException,
    1061            NoSuchMethodException {
    1062
    1063        if (bean == null) {
    1064            throw new IllegalArgumentException("No bean specified");
    1065        }
    1066        if (name == null) {
    1067            throw new IllegalArgumentException("No name specified for bean class '" +
    1068                    bean.getClass() + "'");
    1069        }
    1070
    1071        final PropertyDescriptor descriptor =
    1072                getPropertyDescriptor(bean, name);
    1073        if (descriptor != null) {
    1074            return (descriptor.getPropertyEditorClass());
    1075        } else {
    1076            return (null);
    1077        }
    1078
    1079    }
    1080
    1081
    1082    /**
    1083     * Return the Java Class representing the property type of the specified
    1084     * property, or <code>null</code> if there is no such property for the
    1085     * specified bean.  This method follows the same name resolution rules
    1086     * used by <code>getPropertyDescriptor()</code>, so if the last element
    1087     * of a name reference is indexed, the type of the property itself will
    1088     * be returned.  If the last (or only) element has no property with the
    1089     * specified name, <code>null</code> is returned.
    1090     * <p>
    1091     * If the property is an indexed property (e.g. <code>String[]</code>),
    1092     * this method will return the type of the items within that array.
    1093     * Note that from Java 8 and newer, this method do not support
    1094     * such index types from items within an Collection, and will
    1095     * instead return the collection type (e.g. java.util.List) from the
    1096     * getter mtethod.
    1097     *
    1098     * @param bean Bean for which a property descriptor is requested
    1099     * @param name Possibly indexed and/or nested name of the property for
    1100     *  which a property descriptor is requested
    1101     * @return The property type
    1102     *
    1103     * @throws IllegalAccessException if the caller does not have
    1104     *  access to the property accessor method
    1105     * @throws IllegalArgumentException if <code>bean</code> or
    1106     *  <code>name</code> is null
    1107     * @throws IllegalArgumentException if a nested reference to a
    1108     *  property returns null
    1109     * @throws InvocationTargetException if the property accessor method
    1110     *  throws an exception
    1111     * @throws NoSuchMethodException if an accessor method for this
    1112     *  propety cannot be found
    1113     */
    1114    public Class<?> getPropertyType(Object bean, String name)
    1115            throws IllegalAccessException, InvocationTargetException,
    1116            NoSuchMethodException {
    1117
    1118        if (bean == null) {
    1119            throw new IllegalArgumentException("No bean specified");
    1120        }
    1121        if (name == null) {
    1122            throw new IllegalArgumentException("No name specified for bean class '" +
    1123                    bean.getClass() + "'");
    1124        }
    1125
    1126        // Resolve nested references
    1127        while (resolver.hasNested(name)) {
    1128            final String next = resolver.next(name);
    1129            final Object nestedBean = getProperty(bean, next);
    1130            if (nestedBean == null) {
    1131                throw new NestedNullException
    1132                        ("Null property value for '" + next +
    1133                        "' on bean class '" + bean.getClass() + "'");
    1134            }
    1135            bean = nestedBean;
    1136            name = resolver.remove(name);
    1137        }
    1138
    1139        // Remove any subscript from the final name value
    1140        name = resolver.getProperty(name);
    1141
    1142        // Special handling for DynaBeans
    1143        if (bean instanceof DynaBean) {
    1144            final DynaProperty descriptor =
    1145                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
    1146            if (descriptor == null) {
    1147                return (null);
    1148            }
    1149            final Class<?> type = descriptor.getType();
    1150            if (type == null) {
    1151                return (null);
    1152            } else if (type.isArray()) {
    1153                return (type.getComponentType());
    1154            } else {
    1155                return (type);
    1156            }
    1157        }
    1158
    1159        final PropertyDescriptor descriptor =
    1160                getPropertyDescriptor(bean, name);
    1161        if (descriptor == null) {
    1162            return (null);
    1163        } else if (descriptor instanceof IndexedPropertyDescriptor) {
    1164            return (((IndexedPropertyDescriptor) descriptor).
    1165                    getIndexedPropertyType());
    1166        } else if (descriptor instanceof MappedPropertyDescriptor) {
    1167            return (((MappedPropertyDescriptor) descriptor).
    1168                    getMappedPropertyType());
    1169        } else {
    1170            return (descriptor.getPropertyType());
    1171        }
    1172
    1173    }
    1174
    1175
    1176    /**
    1177     * <p>Return an accessible property getter method for this property,
    1178     * if there is one; otherwise return <code>null</code>.</p>
    1179     *
    1180     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    1181     *
    1182     * @param descriptor Property descriptor to return a getter for
    1183     * @return The read method
    1184     */
    1185    public Method getReadMethod(final PropertyDescriptor descriptor) {
    1186
    1187        return (MethodUtils.getAccessibleMethod(descriptor.getReadMethod()));
    1188
    1189    }
    1190
    1191
    1192    /**
    1193     * <p>Return an accessible property getter method for this property,
    1194     * if there is one; otherwise return <code>null</code>.</p>
    1195     *
    1196     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    1197     *
    1198     * @param clazz The class of the read method will be invoked on
    1199     * @param descriptor Property descriptor to return a getter for
    1200     * @return The read method
    1201     */
    1202    Method getReadMethod(final Class<?> clazz, final PropertyDescriptor descriptor) {
    1203        return (MethodUtils.getAccessibleMethod(clazz, descriptor.getReadMethod()));
    1204    }
    1205
    1206
    1207    /**
    1208     * Return the value of the specified simple property of the specified
    1209     * bean, with no type conversions.
    1210     *
    1211     * @param bean Bean whose property is to be extracted
    1212     * @param name Name of the property to be extracted
    1213     * @return The property value
    1214     *
    1215     * @throws IllegalAccessException if the caller does not have
    1216     *  access to the property accessor method
    1217     * @throws IllegalArgumentException if <code>bean</code> or
    1218     *  <code>name</code> is null
    1219     * @throws IllegalArgumentException if the property name
    1220     *  is nested or indexed
    1221     * @throws InvocationTargetException if the property accessor method
    1222     *  throws an exception
    1223     * @throws NoSuchMethodException if an accessor method for this
    1224     *  propety cannot be found
    1225     */
    1226    public Object getSimpleProperty(final Object bean, final String name)
    1227            throws IllegalAccessException, InvocationTargetException,
    1228            NoSuchMethodException {
    1229
    1230        if (bean == null) {
    1231            throw new IllegalArgumentException("No bean specified");
    1232        }
    1233        if (name == null) {
    1234            throw new IllegalArgumentException("No name specified for bean class '" +
    1235                    bean.getClass() + "'");
    1236        }
    1237
    1238        // Validate the syntax of the property name
    1239        if (resolver.hasNested(name)) {
    1240            throw new IllegalArgumentException
    1241                    ("Nested property names are not allowed: Property '" +
    1242                    name + "' on bean class '" + bean.getClass() + "'");
    1243        } else if (resolver.isIndexed(name)) {
    1244            throw new IllegalArgumentException
    1245                    ("Indexed property names are not allowed: Property '" +
    1246                    name + "' on bean class '" + bean.getClass() + "'");
    1247        } else if (resolver.isMapped(name)) {
    1248            throw new IllegalArgumentException
    1249                    ("Mapped property names are not allowed: Property '" +
    1250                    name + "' on bean class '" + bean.getClass() + "'");
    1251        }
    1252
    1253        // Handle DynaBean instances specially
    1254        if (bean instanceof DynaBean) {
    1255            final DynaProperty descriptor =
    1256                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
    1257            if (descriptor == null) {
    1258                throw new NoSuchMethodException("Unknown property '" +
    1259                        name + "' on dynaclass '" +
    1260                        ((DynaBean) bean).getDynaClass() + "'" );
    1261            }
    1262            return (((DynaBean) bean).get(name));
    1263        }
    1264
    1265        // Retrieve the property getter method for the specified property
    1266        final PropertyDescriptor descriptor =
    1267                getPropertyDescriptor(bean, name);
    1268        if (descriptor == null) {
    1269            throw new NoSuchMethodException("Unknown property '" +
    1270                    name + "' on class '" + bean.getClass() + "'" );
    1271        }
    1272        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
    1273        if (readMethod == null) {
    1274            throw new NoSuchMethodException("Property '" + name +
    1275                    "' has no getter method in class '" + bean.getClass() + "'");
    1276        }
    1277
    1278        // Call the property getter and return the value
    1279        final Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
    1280        return (value);
    1281
    1282    }
    1283
    1284
    1285    /**
    1286     * <p>Return an accessible property setter method for this property,
    1287     * if there is one; otherwise return <code>null</code>.</p>
    1288     *
    1289     * <p><em>Note:</em> This method does not work correctly with custom bean
    1290     * introspection under certain circumstances. It may return {@code null}
    1291     * even if a write method is defined for the property in question. Use
    1292     * {@link #getWriteMethod(Class, PropertyDescriptor)} to be sure that the
    1293     * correct result is returned.</p>
    1294     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    1295     *
    1296     * @param descriptor Property descriptor to return a setter for
    1297     * @return The write method
    1298     */
    1299    public Method getWriteMethod(final PropertyDescriptor descriptor) {
    1300
    1301        return (MethodUtils.getAccessibleMethod(descriptor.getWriteMethod()));
    1302
    1303    }
    1304
    1305
    1306    /**
    1307     * <p>Return an accessible property setter method for this property,
    1308     * if there is one; otherwise return <code>null</code>.</p>
    1309     *
    1310     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
    1311     *
    1312     * @param clazz The class of the read method will be invoked on
    1313     * @param descriptor Property descriptor to return a setter for
    1314     * @return The write method
    1315     * @since 1.9.1
    1316     */
    1317    public Method getWriteMethod(final Class<?> clazz, final PropertyDescriptor descriptor) {
    1318        final BeanIntrospectionData data = getIntrospectionData(clazz);
    1319        return (MethodUtils.getAccessibleMethod(clazz,
    1320                data.getWriteMethod(clazz, descriptor)));
    1321    }
    1322
    1323
    1324    /**
    1325     * <p>Return <code>true</code> if the specified property name identifies
    1326     * a readable property on the specified bean; otherwise, return
    1327     * <code>false</code>.
    1328     *
    1329     * @param bean Bean to be examined (may be a {@link DynaBean}
    1330     * @param name Property name to be evaluated
    1331     * @return <code>true</code> if the property is readable,
    1332     * otherwise <code>false</code>
    1333     *
    1334     * @throws IllegalArgumentException if <code>bean</code>
    1335     *  or <code>name</code> is <code>null</code>
    1336     *
    1337     * @since BeanUtils 1.6
    1338     */
    1339    public boolean isReadable(Object bean, String name) {
    1340
    1341        // Validate method parameters
    1342        if (bean == null) {
    1343            throw new IllegalArgumentException("No bean specified");
    1344        }
    1345        if (name == null) {
    1346            throw new IllegalArgumentException("No name specified for bean class '" +
    1347                    bean.getClass() + "'");
    1348        }
    1349
    1350        // Resolve nested references
    1351        while (resolver.hasNested(name)) {
    1352            final String next = resolver.next(name);
    1353            Object nestedBean = null;
    1354            try {
    1355                nestedBean = getProperty(bean, next);
    1356            } catch (final IllegalAccessException e) {
    1357                return false;
    1358            } catch (final InvocationTargetException e) {
    1359                return false;
    1360            } catch (final NoSuchMethodException e) {
    1361                return false;
    1362            }
    1363            if (nestedBean == null) {
    1364                throw new NestedNullException
    1365                        ("Null property value for '" + next +
    1366                        "' on bean class '" + bean.getClass() + "'");
    1367            }
    1368            bean = nestedBean;
    1369            name = resolver.remove(name);
    1370        }
    1371
    1372        // Remove any subscript from the final name value
    1373        name = resolver.getProperty(name);
    1374
    1375        // Treat WrapDynaBean as special case - may be a write-only property
    1376        // (see Jira issue# BEANUTILS-61)
    1377        if (bean instanceof WrapDynaBean) {
    1378            bean = ((WrapDynaBean)bean).getInstance();
    1379        }
    1380
    1381        // Return the requested result
    1382        if (bean instanceof DynaBean) {
    1383            // All DynaBean properties are readable
    1384            return (((DynaBean) bean).getDynaClass().getDynaProperty(name) != null);
    1385        } else {
    1386            try {
    1387                final PropertyDescriptor desc =
    1388                    getPropertyDescriptor(bean, name);
    1389                if (desc != null) {
    1390                    Method readMethod = getReadMethod(bean.getClass(), desc);
    1391                    if (readMethod == null) {
    1392                        if (desc instanceof IndexedPropertyDescriptor) {
    1393                            readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod();
    1394                        } else if (desc instanceof MappedPropertyDescriptor) {
    1395                            readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod();
    1396                        }
    1397                        readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
    1398                    }
    1399                    return (readMethod != null);
    1400                } else {
    1401                    return (false);
    1402                }
    1403            } catch (final IllegalAccessException e) {
    1404                return (false);
    1405            } catch (final InvocationTargetException e) {
    1406                return (false);
    1407            } catch (final NoSuchMethodException e) {
    1408                return (false);
    1409            }
    1410        }
    1411
    1412    }
    1413
    1414
    1415    /**
    1416     * <p>Return <code>true</code> if the specified property name identifies
    1417     * a writeable property on the specified bean; otherwise, return
    1418     * <code>false</code>.
    1419     *
    1420     * @param bean Bean to be examined (may be a {@link DynaBean}
    1421     * @param name Property name to be evaluated
    1422     * @return <code>true</code> if the property is writeable,
    1423     * otherwise <code>false</code>
    1424     *
    1425     * @throws IllegalArgumentException if <code>bean</code>
    1426     *  or <code>name</code> is <code>null</code>
    1427     *
    1428     * @since BeanUtils 1.6
    1429     */
    1430    public boolean isWriteable(Object bean, String name) {
    1431
    1432        // Validate method parameters
    1433        if (bean == null) {
    1434            throw new IllegalArgumentException("No bean specified");
    1435        }
    1436        if (name == null) {
    1437            throw new IllegalArgumentException("No name specified for bean class '" +
    1438                    bean.getClass() + "'");
    1439        }
    1440
    1441        // Resolve nested references
    1442        while (resolver.hasNested(name)) {
    1443            final String next = resolver.next(name);
    1444            Object nestedBean = null;
    1445            try {
    1446                nestedBean = getProperty(bean, next);
    1447            } catch (final IllegalAccessException e) {
    1448                return false;
    1449            } catch (final InvocationTargetException e) {
    1450                return false;
    1451            } catch (final NoSuchMethodException e) {
    1452                return false;
    1453            }
    1454            if (nestedBean == null) {
    1455                throw new NestedNullException
    1456                        ("Null property value for '" + next +
    1457                        "' on bean class '" + bean.getClass() + "'");
    1458            }
    1459            bean = nestedBean;
    1460            name = resolver.remove(name);
    1461        }
    1462
    1463        // Remove any subscript from the final name value
    1464        name = resolver.getProperty(name);
    1465
    1466        // Treat WrapDynaBean as special case - may be a read-only property
    1467        // (see Jira issue# BEANUTILS-61)
    1468        if (bean instanceof WrapDynaBean) {
    1469            bean = ((WrapDynaBean)bean).getInstance();
    1470        }
    1471
    1472        // Return the requested result
    1473        if (bean instanceof DynaBean) {
    1474            // All DynaBean properties are writeable
    1475            return (((DynaBean) bean).getDynaClass().getDynaProperty(name) != null);
    1476        } else {
    1477            try {
    1478                final PropertyDescriptor desc =
    1479                    getPropertyDescriptor(bean, name);
    1480                if (desc != null) {
    1481                    Method writeMethod = getWriteMethod(bean.getClass(), desc);
    1482                    if (writeMethod == null) {
    1483                        if (desc instanceof IndexedPropertyDescriptor) {
    1484                            writeMethod = ((IndexedPropertyDescriptor) desc).getIndexedWriteMethod();
    1485                        } else if (desc instanceof MappedPropertyDescriptor) {
    1486                            writeMethod = ((MappedPropertyDescriptor) desc).getMappedWriteMethod();
    1487                        }
    1488                        writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
    1489                    }
    1490                    return (writeMethod != null);
    1491                } else {
    1492                    return (false);
    1493                }
    1494            } catch (final IllegalAccessException e) {
    1495                return (false);
    1496            } catch (final InvocationTargetException e) {
    1497                return (false);
    1498            } catch (final NoSuchMethodException e) {
    1499                return (false);
    1500            }
    1501        }
    1502
    1503    }
    1504
    1505
    1506    /**
    1507     * Set the value of the specified indexed property of the specified
    1508     * bean, with no type conversions.  The zero-relative index of the
    1509     * required value must be included (in square brackets) as a suffix to
    1510     * the property name, or <code>IllegalArgumentException</code> will be
    1511     * thrown.  In addition to supporting the JavaBeans specification, this
    1512     * method has been extended to support <code>List</code> objects as well.
    1513     *
    1514     * @param bean Bean whose property is to be modified
    1515     * @param name <code>propertyname[index]</code> of the property value
    1516     *  to be modified
    1517     * @param value Value to which the specified property element
    1518     *  should be set
    1519     *
    1520     * @throws IndexOutOfBoundsException if the specified index
    1521     *  is outside the valid range for the underlying property
    1522     * @throws IllegalAccessException if the caller does not have
    1523     *  access to the property accessor method
    1524     * @throws IllegalArgumentException if <code>bean</code> or
    1525     *  <code>name</code> is null
    1526     * @throws InvocationTargetException if the property accessor method
    1527     *  throws an exception
    1528     * @throws NoSuchMethodException if an accessor method for this
    1529     *  propety cannot be found
    1530     */
    1531    public void setIndexedProperty(final Object bean, String name,
    1532                                          final Object value)
    1533            throws IllegalAccessException, InvocationTargetException,
    1534            NoSuchMethodException {
    1535
    1536        if (bean == null) {
    1537            throw new IllegalArgumentException("No bean specified");
    1538        }
    1539        if (name == null) {
    1540            throw new IllegalArgumentException("No name specified for bean class '" +
    1541                    bean.getClass() + "'");
    1542        }
    1543
    1544        // Identify the index of the requested individual property
    1545        int index = -1;
    1546        try {
    1547            index = resolver.getIndex(name);
    1548        } catch (final IllegalArgumentException e) {
    1549            throw new IllegalArgumentException("Invalid indexed property '" +
    1550                    name + "' on bean class '" + bean.getClass() + "'");
    1551        }
    1552        if (index < 0) {
    1553            throw new IllegalArgumentException("Invalid indexed property '" +
    1554                    name + "' on bean class '" + bean.getClass() + "'");
    1555        }
    1556
    1557        // Isolate the name
    1558        name = resolver.getProperty(name);
    1559
    1560        // Set the specified indexed property value
    1561        setIndexedProperty(bean, name, index, value);
    1562
    1563    }
    1564
    1565
    1566    /**
    1567     * Set the value of the specified indexed property of the specified
    1568     * bean, with no type conversions.  In addition to supporting the JavaBeans
    1569     * specification, this method has been extended to support
    1570     * <code>List</code> objects as well.
    1571     *
    1572     * @param bean Bean whose property is to be set
    1573     * @param name Simple property name of the property value to be set
    1574     * @param index Index of the property value to be set
    1575     * @param value Value to which the indexed property element is to be set
    1576     *
    1577     * @throws IndexOutOfBoundsException if the specified index
    1578     *  is outside the valid range for the underlying property
    1579     * @throws IllegalAccessException if the caller does not have
    1580     *  access to the property accessor method
    1581     * @throws IllegalArgumentException if <code>bean</code> or
    1582     *  <code>name</code> is null
    1583     * @throws InvocationTargetException if the property accessor method
    1584     *  throws an exception
    1585     * @throws NoSuchMethodException if an accessor method for this
    1586     *  propety cannot be found
    1587     */
    1588    public void setIndexedProperty(final Object bean, final String name,
    1589                                          final int index, final Object value)
    1590            throws IllegalAccessException, InvocationTargetException,
    1591            NoSuchMethodException {
    1592
    1593        if (bean == null) {
    1594            throw new IllegalArgumentException("No bean specified");
    1595        }
    1596        if (name == null || name.length() == 0) {
    1597            if (bean.getClass().isArray()) {
    1598                Array.set(bean, index, value);
    1599                return;
    1600            } else if (bean instanceof List) {
    1601                final List<Object> list = toObjectList(bean);
    1602                list.set(index, value);
    1603                return;
    1604            }
    1605        }
    1606        if (name == null) {
    1607            throw new IllegalArgumentException("No name specified for bean class '" +
    1608                    bean.getClass() + "'");
    1609        }
    1610
    1611        // Handle DynaBean instances specially
    1612        if (bean instanceof DynaBean) {
    1613            final DynaProperty descriptor =
    1614                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
    1615            if (descriptor == null) {
    1616                throw new NoSuchMethodException("Unknown property '" +
    1617                        name + "' on bean class '" + bean.getClass() + "'");
    1618            }
    1619            ((DynaBean) bean).set(name, index, value);
    1620            return;
    1621        }
    1622
    1623        // Retrieve the property descriptor for the specified property
    1624        final PropertyDescriptor descriptor =
    1625                getPropertyDescriptor(bean, name);
    1626        if (descriptor == null) {
    1627            throw new NoSuchMethodException("Unknown property '" +
    1628                    name + "' on bean class '" + bean.getClass() + "'");
    1629        }
    1630
    1631        // Call the indexed setter method if there is one
    1632        if (descriptor instanceof IndexedPropertyDescriptor) {
    1633            Method writeMethod = ((IndexedPropertyDescriptor) descriptor).
    1634                    getIndexedWriteMethod();
    1635            writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
    1636            if (writeMethod != null) {
    1637                final Object[] subscript = new Object[2];
    1638                subscript[0] = new Integer(index);
    1639                subscript[1] = value;
    1640                try {
    1641                    if (log.isTraceEnabled()) {
    1642                        final String valueClassName =
    1643                            value == null ? "<null>"
    1644                                          : value.getClass().getName();
    1645                        log.trace("setSimpleProperty: Invoking method "
    1646                                  + writeMethod +" with index=" + index
    1647                                  + ", value=" + value
    1648                                  + " (class " + valueClassName+ ")");
    1649                    }
    1650                    invokeMethod(writeMethod, bean, subscript);
    1651                } catch (final InvocationTargetException e) {
    1652                    if (e.getTargetException() instanceof
    1653                            IndexOutOfBoundsException) {
    1654                        throw (IndexOutOfBoundsException)
    1655                                e.getTargetException();
    1656                    } else {
    1657                        throw e;
    1658                    }
    1659                }
    1660                return;
    1661            }
    1662        }
    1663
    1664        // Otherwise, the underlying property must be an array or a list
    1665        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
    1666        if (readMethod == null) {
    1667            throw new NoSuchMethodException("Property '" + name +
    1668                    "' has no getter method on bean class '" + bean.getClass() + "'");
    1669        }
    1670
    1671        // Call the property getter to get the array or list
    1672        final Object array = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
    1673        if (!array.getClass().isArray()) {
    1674            if (array instanceof List) {
    1675                // Modify the specified value in the List
    1676                final List<Object> list = toObjectList(array);
    1677                list.set(index, value);
    1678            } else {
    1679                throw new IllegalArgumentException("Property '" + name +
    1680                        "' is not indexed on bean class '" + bean.getClass() + "'");
    1681            }
    1682        } else {
    1683            // Modify the specified value in the array
    1684            Array.set(array, index, value);
    1685        }
    1686
    1687    }
    1688
    1689
    1690    /**
    1691     * Set the value of the specified mapped property of the
    1692     * specified bean, with no type conversions.  The key of the
    1693     * value to set must be included (in brackets) as a suffix to
    1694     * the property name, or <code>IllegalArgumentException</code> will be
    1695     * thrown.
    1696     *
    1697     * @param bean Bean whose property is to be set
    1698     * @param name <code>propertyname(key)</code> of the property value
    1699     *  to be set
    1700     * @param value The property value to be set
    1701     *
    1702     * @throws IllegalAccessException if the caller does not have
    1703     *  access to the property accessor method
    1704     * @throws InvocationTargetException if the property accessor method
    1705     *  throws an exception
    1706     * @throws NoSuchMethodException if an accessor method for this
    1707     *  propety cannot be found
    1708     */
    1709    public void setMappedProperty(final Object bean, String name,
    1710                                         final Object value)
    1711            throws IllegalAccessException, InvocationTargetException,
    1712            NoSuchMethodException {
    1713
    1714        if (bean == null) {
    1715            throw new IllegalArgumentException("No bean specified");
    1716        }
    1717        if (name == null) {
    1718            throw new IllegalArgumentException("No name specified for bean class '" +
    1719                    bean.getClass() + "'");
    1720        }
    1721
    1722        // Identify the key of the requested individual property
    1723        String key  = null;
    1724        try {
    1725            key = resolver.getKey(name);
    1726        } catch (final IllegalArgumentException e) {
    1727            throw new IllegalArgumentException
    1728                    ("Invalid mapped property '" + name +
    1729                    "' on bean class '" + bean.getClass() + "'");
    1730        }
    1731        if (key == null) {
    1732            throw new IllegalArgumentException
    1733                    ("Invalid mapped property '" + name +
    1734                    "' on bean class '" + bean.getClass() + "'");
    1735        }
    1736
    1737        // Isolate the name
    1738        name = resolver.getProperty(name);
    1739
    1740        // Request the specified indexed property value
    1741        setMappedProperty(bean, name, key, value);
    1742
    1743    }
    1744
    1745
    1746    /**
    1747     * Set the value of the specified mapped property of the specified
    1748     * bean, with no type conversions.
    1749     *
    1750     * @param bean Bean whose property is to be set
    1751     * @param name Mapped property name of the property value to be set
    1752     * @param key Key of the property value to be set
    1753     * @param value The property value to be set
    1754     *
    1755     * @throws IllegalAccessException if the caller does not have
    1756     *  access to the property accessor method
    1757     * @throws InvocationTargetException if the property accessor method
    1758     *  throws an exception
    1759     * @throws NoSuchMethodException if an accessor method for this
    1760     *  propety cannot be found
    1761     */
    1762    public void setMappedProperty(final Object bean, final String name,
    1763                                         final String key, final Object value)
    1764            throws IllegalAccessException, InvocationTargetException,
    1765            NoSuchMethodException {
    1766
    1767        if (bean == null) {
    1768            throw new IllegalArgumentException("No bean specified");
    1769        }
    1770        if (name == null) {
    1771            throw new IllegalArgumentException("No name specified for bean class '" +
    1772                    bean.getClass() + "'");
    1773        }
    1774        if (key == null) {
    1775            throw new IllegalArgumentException("No key specified for property '" +
    1776                    name + "' on bean class '" + bean.getClass() + "'");
    1777        }
    1778
    1779        // Handle DynaBean instances specially
    1780        if (bean instanceof DynaBean) {
    1781            final DynaProperty descriptor =
    1782                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
    1783            if (descriptor == null) {
    1784                throw new NoSuchMethodException("Unknown property '" +
    1785                        name + "' on bean class '" + bean.getClass() + "'");
    1786            }
    1787            ((DynaBean) bean).set(name, key, value);
    1788            return;
    1789        }
    1790
    1791        // Retrieve the property descriptor for the specified property
    1792        final PropertyDescriptor descriptor =
    1793                getPropertyDescriptor(bean, name);
    1794        if (descriptor == null) {
    1795            throw new NoSuchMethodException("Unknown property '" +
    1796                    name + "' on bean class '" + bean.getClass() + "'");
    1797        }
    1798
    1799        if (descriptor instanceof MappedPropertyDescriptor) {
    1800            // Call the keyed setter method if there is one
    1801            Method mappedWriteMethod =
    1802                    ((MappedPropertyDescriptor) descriptor).
    1803                    getMappedWriteMethod();
    1804            mappedWriteMethod = MethodUtils.getAccessibleMethod(bean.getClass(), mappedWriteMethod);
    1805            if (mappedWriteMethod != null) {
    1806                final Object[] params = new Object[2];
    1807                params[0] = key;
    1808                params[1] = value;
    1809                if (log.isTraceEnabled()) {
    1810                    final String valueClassName =
    1811                        value == null ? "<null>" : value.getClass().getName();
    1812                    log.trace("setSimpleProperty: Invoking method "
    1813                              + mappedWriteMethod + " with key=" + key
    1814                              + ", value=" + value
    1815                              + " (class " + valueClassName +")");
    1816                }
    1817                invokeMethod(mappedWriteMethod, bean, params);
    1818            } else {
    1819                throw new NoSuchMethodException
    1820                    ("Property '" + name + "' has no mapped setter method" +
    1821                     "on bean class '" + bean.getClass() + "'");
    1822            }
    1823        } else {
    1824          /* means that the result has to be retrieved from a map */
    1825          final Method readMethod = getReadMethod(bean.getClass(), descriptor);
    1826          if (readMethod != null) {
    1827            final Object invokeResult = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
    1828            /* test and fetch from the map */
    1829            if (invokeResult instanceof java.util.Map) {
    1830              final java.util.Map<String, Object> map = toPropertyMap(invokeResult);
    1831              map.put(key, value);
    1832            }
    1833          } else {
    1834            throw new NoSuchMethodException("Property '" + name +
    1835                    "' has no mapped getter method on bean class '" +
    1836                    bean.getClass() + "'");
    1837          }
    1838        }
    1839
    1840    }
    1841
    1842
    1843    /**
    1844     * Set the value of the (possibly nested) property of the specified
    1845     * name, for the specified bean, with no type conversions.
    1846     * <p>
    1847     * Example values for parameter "name" are:
    1848     * <ul>
    1849     * <li> "a" -- sets the value of property a of the specified bean </li>
    1850     * <li> "a.b" -- gets the value of property a of the specified bean,
    1851     * then on that object sets the value of property b.</li>
    1852     * <li> "a(key)" -- sets a value of mapped-property a on the specified
    1853     * bean. This effectively means bean.setA("key").</li>
    1854     * <li> "a[3]" -- sets a value of indexed-property a on the specified
    1855     * bean. This effectively means bean.setA(3).</li>
    1856     * </ul>
    1857     *
    1858     * @param bean Bean whose property is to be modified
    1859     * @param name Possibly nested name of the property to be modified
    1860     * @param value Value to which the property is to be set
    1861     *
    1862     * @throws IllegalAccessException if the caller does not have
    1863     *  access to the property accessor method
    1864     * @throws IllegalArgumentException if <code>bean</code> or
    1865     *  <code>name</code> is null
    1866     * @throws IllegalArgumentException if a nested reference to a
    1867     *  property returns null
    1868     * @throws InvocationTargetException if the property accessor method
    1869     *  throws an exception
    1870     * @throws NoSuchMethodException if an accessor method for this
    1871     *  propety cannot be found
    1872     */
    1873    public void setNestedProperty(Object bean,
    1874                                         String name, final Object value)
    1875            throws IllegalAccessException, InvocationTargetException,
    1876            NoSuchMethodException {
    1877
    1878        if (bean == null) {
    1879            throw new IllegalArgumentException("No bean specified");
    1880        }
    1881        if (name == null) {
    1882            throw new IllegalArgumentException("No name specified for bean class '" +
    1883                    bean.getClass() + "'");
    1884        }
    1885
    1886        // Resolve nested references
    1887        while (resolver.hasNested(name)) {
    1888            final String next = resolver.next(name);
    1889            Object nestedBean = null;
    1890            if (bean instanceof Map) {
    1891                nestedBean = getPropertyOfMapBean((Map<?, ?>)bean, next);
    1892            } else if (resolver.isMapped(next)) {
    1893                nestedBean = getMappedProperty(bean, next);
    1894            } else if (resolver.isIndexed(next)) {
    1895                nestedBean = getIndexedProperty(bean, next);
    1896            } else {
    1897                nestedBean = getSimpleProperty(bean, next);
    1898            }
    1899            if (nestedBean == null) {
    1900                throw new NestedNullException
    1901                        ("Null property value for '" + name +
    1902                         "' on bean class '" + bean.getClass() + "'");
    1903            }
    1904            bean = nestedBean;
    1905            name = resolver.remove(name);
    1906        }
    1907
    1908        if (bean instanceof Map) {
    1909            setPropertyOfMapBean(toPropertyMap(bean), name, value);
    1910        } else if (resolver.isMapped(name)) {
    1911            setMappedProperty(bean, name, value);
    1912        } else if (resolver.isIndexed(name)) {
    1913            setIndexedProperty(bean, name, value);
    1914        } else {
    1915            setSimpleProperty(bean, name, value);
    1916        }
    1917
    1918    }
    1919
    1920    /**
    1921     * This method is called by method setNestedProperty when the current bean
    1922     * is found to be a Map object, and defines how to deal with setting
    1923     * a property on a Map.
    1924     * <p>
    1925     * The standard implementation here is to:
    1926     * <ul>
    1927     * <li>call bean.set(propertyName) for all propertyName values.</li>
    1928     * <li>throw an IllegalArgumentException if the property specifier
    1929     * contains MAPPED_DELIM or INDEXED_DELIM, as Map entries are essentially
    1930     * simple properties; mapping and indexing operations do not make sense
    1931     * when accessing a map (even thought the returned object may be a Map
    1932     * or an Array).</li>
    1933     * </ul>
    1934     * <p>
    1935     * The default behaviour of beanutils 1.7.1 or later is for assigning to
    1936     * "a.b" to mean a.put(b, obj) always. However the behaviour of beanutils
    1937     * version 1.6.0, 1.6.1, 1.7.0 was for "a.b" to mean a.setB(obj) if such
    1938     * a method existed, and a.put(b, obj) otherwise. In version 1.5 it meant
    1939     * a.put(b, obj) always (ie the same as the behaviour in the current version).
    1940     * In versions prior to 1.5 it meant a.setB(obj) always. [yes, this is
    1941     * all <i>very</i> unfortunate]
    1942     * <p>
    1943     * Users who would like to customise the meaning of "a.b" in method
    1944     * setNestedProperty when a is a Map can create a custom subclass of
    1945     * this class and override this method to implement the behaviour of
    1946     * their choice, such as restoring the pre-1.4 behaviour of this class
    1947     * if they wish. When overriding this method, do not forget to deal
    1948     * with MAPPED_DELIM and INDEXED_DELIM characters in the propertyName.
    1949     * <p>
    1950     * Note, however, that the recommended solution for objects that
    1951     * implement Map but want their simple properties to come first is
    1952     * for <i>those</i> objects to override their get/put methods to implement
    1953     * that behaviour, and <i>not</i> to solve the problem by modifying the
    1954     * default behaviour of the PropertyUtilsBean class by overriding this
    1955     * method.
    1956     *
    1957     * @param bean Map bean
    1958     * @param propertyName The property name
    1959     * @param value the property value
    1960     *
    1961     * @throws IllegalArgumentException when the propertyName is regarded as
    1962     * being invalid.
    1963     *
    1964     * @throws IllegalAccessException just in case subclasses override this
    1965     * method to try to access real setter methods and find permission is denied.
    1966     *
    1967     * @throws InvocationTargetException just in case subclasses override this
    1968     * method to try to access real setter methods, and find it throws an
    1969     * exception when invoked.
    1970     *
    1971     * @throws NoSuchMethodException just in case subclasses override this
    1972     * method to try to access real setter methods, and want to fail if
    1973     * no simple method is available.
    1974     * @since 1.8.0
    1975     */
    1976    protected void setPropertyOfMapBean(final Map<String, Object> bean, String propertyName, final Object value)
    1977        throws IllegalArgumentException, IllegalAccessException,
    1978        InvocationTargetException, NoSuchMethodException {
    1979
    1980        if (resolver.isMapped(propertyName)) {
    1981            final String name = resolver.getProperty(propertyName);
    1982            if (name == null || name.length() == 0) {
    1983                propertyName = resolver.getKey(propertyName);
    1984            }
    1985        }
    1986
    1987        if (resolver.isIndexed(propertyName) ||
    1988            resolver.isMapped(propertyName)) {
    1989            throw new IllegalArgumentException(
    1990                    "Indexed or mapped properties are not supported on"
    1991                    + " objects of type Map: " + propertyName);
    1992        }
    1993
    1994        bean.put(propertyName, value);
    1995    }
    1996
    1997
    1998
    1999    /**
    2000     * Set the value of the specified property of the specified bean,
    2001     * no matter which property reference format is used, with no
    2002     * type conversions.
    2003     *
    2004     * @param bean Bean whose property is to be modified
    2005     * @param name Possibly indexed and/or nested name of the property
    2006     *  to be modified
    2007     * @param value Value to which this property is to be set
    2008     *
    2009     * @throws IllegalAccessException if the caller does not have
    2010     *  access to the property accessor method
    2011     * @throws IllegalArgumentException if <code>bean</code> or
    2012     *  <code>name</code> is null
    2013     * @throws InvocationTargetException if the property accessor method
    2014     *  throws an exception
    2015     * @throws NoSuchMethodException if an accessor method for this
    2016     *  propety cannot be found
    2017     */
    2018    public void setProperty(final Object bean, final String name, final Object value)
    2019            throws IllegalAccessException, InvocationTargetException,
    2020            NoSuchMethodException {
    2021
    2022        setNestedProperty(bean, name, value);
    2023
    2024    }
    2025
    2026
    2027    /**
    2028     * Set the value of the specified simple property of the specified bean,
    2029     * with no type conversions.
    2030     *
    2031     * @param bean Bean whose property is to be modified
    2032     * @param name Name of the property to be modified
    2033     * @param value Value to which the property should be set
    2034     *
    2035     * @throws IllegalAccessException if the caller does not have
    2036     *  access to the property accessor method
    2037     * @throws IllegalArgumentException if <code>bean</code> or
    2038     *  <code>name</code> is null
    2039     * @throws IllegalArgumentException if the property name is
    2040     *  nested or indexed
    2041     * @throws InvocationTargetException if the property accessor method
    2042     *  throws an exception
    2043     * @throws NoSuchMethodException if an accessor method for this
    2044     *  propety cannot be found
    2045     */
    2046    public void setSimpleProperty(final Object bean,
    2047                                         final String name, final Object value)
    2048            throws IllegalAccessException, InvocationTargetException,
    2049            NoSuchMethodException {
    2050
    2051        if (bean == null) {
    2052            throw new IllegalArgumentException("No bean specified");
    2053        }
    2054        if (name == null) {
    2055            throw new IllegalArgumentException("No name specified for bean class '" +
    2056                    bean.getClass() + "'");
    2057        }
    2058
    2059        // Validate the syntax of the property name
    2060        if (resolver.hasNested(name)) {
    2061            throw new IllegalArgumentException
    2062                    ("Nested property names are not allowed: Property '" +
    2063                    name + "' on bean class '" + bean.getClass() + "'");
    2064        } else if (resolver.isIndexed(name)) {
    2065            throw new IllegalArgumentException
    2066                    ("Indexed property names are not allowed: Property '" +
    2067                    name + "' on bean class '" + bean.getClass() + "'");
    2068        } else if (resolver.isMapped(name)) {
    2069            throw new IllegalArgumentException
    2070                    ("Mapped property names are not allowed: Property '" +
    2071                    name + "' on bean class '" + bean.getClass() + "'");
    2072        }
    2073
    2074        // Handle DynaBean instances specially
    2075        if (bean instanceof DynaBean) {
    2076            final DynaProperty descriptor =
    2077                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
    2078            if (descriptor == null) {
    2079                throw new NoSuchMethodException("Unknown property '" +
    2080                        name + "' on dynaclass '" +
    2081                        ((DynaBean) bean).getDynaClass() + "'" );
    2082            }
    2083            ((DynaBean) bean).set(name, value);
    2084            return;
    2085        }
    2086
    2087        // Retrieve the property setter method for the specified property
    2088        final PropertyDescriptor descriptor =
    2089                getPropertyDescriptor(bean, name);
    2090        if (descriptor == null) {
    2091            throw new NoSuchMethodException("Unknown property '" +
    2092                    name + "' on class '" + bean.getClass() + "'" );
    2093        }
    2094        final Method writeMethod = getWriteMethod(bean.getClass(), descriptor);
    2095        if (writeMethod == null) {
    2096            throw new NoSuchMethodException("Property '" + name +
    2097                    "' has no setter method in class '" + bean.getClass() + "'");
    2098        }
    2099
    2100        // Call the property setter method
    2101        final Object[] values = new Object[1];
    2102        values[0] = value;
    2103        if (log.isTraceEnabled()) {
    2104            final String valueClassName =
    2105                value == null ? "<null>" : value.getClass().getName();
    2106            log.trace("setSimpleProperty: Invoking method " + writeMethod
    2107                      + " with value " + value + " (class " + valueClassName + ")");
    2108        }
    2109        invokeMethod(writeMethod, bean, values);
    2110
    2111    }
    2112
    2113    /** This just catches and wraps IllegalArgumentException. */
    2114    private Object invokeMethod(
    2115                        final Method method,
    2116                        final Object bean,
    2117                        final Object[] values)
    2118                            throws
    2119                                IllegalAccessException,
    2120                                InvocationTargetException {
    2121        if(bean == null) {
    2122            throw new IllegalArgumentException("No bean specified " +
    2123                "- this should have been checked before reaching this method");
    2124        }
    2125
    2126        try {
    2127
    2128            return method.invoke(bean, values);
    2129
    2130        } catch (final NullPointerException cause) {
    2131            // JDK 1.3 and JDK 1.4 throw NullPointerException if an argument is
    2132            // null for a primitive value (JDK 1.5+ throw IllegalArgumentException)
    2133            String valueString = "";
    2134            if (values != null) {
    2135                for (int i = 0; i < values.length; i++) {
    2136                    if (i>0) {
    2137                        valueString += ", " ;
    2138                    }
    2139                    if (values[i] == null) {
    2140                        valueString += "<null>";
    2141                    } else {
    2142                        valueString += (values[i]).getClass().getName();
    2143                    }
    2144                }
    2145            }
    2146            String expectedString = "";
    2147            final Class<?>[] parTypes = method.getParameterTypes();
    2148            if (parTypes != null) {
    2149                for (int i = 0; i < parTypes.length; i++) {
    2150                    if (i > 0) {
    2151                        expectedString += ", ";
    2152                    }
    2153                    expectedString += parTypes[i].getName();
    2154                }
    2155            }
    2156            final IllegalArgumentException e = new IllegalArgumentException(
    2157                "Cannot invoke " + method.getDeclaringClass().getName() + "."
    2158                + method.getName() + " on bean class '" + bean.getClass() +
    2159                "' - " + cause.getMessage()
    2160                // as per https://issues.apache.org/jira/browse/BEANUTILS-224
    2161                + " - had objects of type \"" + valueString
    2162                + "\" but expected signature \""
    2163                +   expectedString + "\""
    2164                );
    2165            if (!BeanUtils.initCause(e, cause)) {
    2166                log.error("Method invocation failed", cause);
    2167            }
    2168            throw e;
    2169        } catch (final IllegalArgumentException cause) {
    2170            String valueString = "";
    2171            if (values != null) {
    2172                for (int i = 0; i < values.length; i++) {
    2173                    if (i>0) {
    2174                        valueString += ", " ;
    2175                    }
    2176                    if (values[i] == null) {
    2177                        valueString += "<null>";
    2178                    } else {
    2179                        valueString += (values[i]).getClass().getName();
    2180                    }
    2181                }
    2182            }
    2183            String expectedString = "";
    2184            final Class<?>[] parTypes = method.getParameterTypes();
    2185            if (parTypes != null) {
    2186                for (int i = 0; i < parTypes.length; i++) {
    2187                    if (i > 0) {
    2188                        expectedString += ", ";
    2189                    }
    2190                    expectedString += parTypes[i].getName();
    2191                }
    2192            }
    2193            final IllegalArgumentException e = new IllegalArgumentException(
    2194                "Cannot invoke " + method.getDeclaringClass().getName() + "."
    2195                + method.getName() + " on bean class '" + bean.getClass() +
    2196                "' - " + cause.getMessage()
    2197                // as per https://issues.apache.org/jira/browse/BEANUTILS-224
    2198                + " - had objects of type \"" + valueString
    2199                + "\" but expected signature \""
    2200                +   expectedString + "\""
    2201                );
    2202            if (!BeanUtils.initCause(e, cause)) {
    2203                log.error("Method invocation failed", cause);
    2204            }
    2205            throw e;
    2206
    2207        }
    2208    }
    2209
    2210    /**
    2211     * Obtains the {@code BeanIntrospectionData} object describing the specified bean
    2212     * class. This object is looked up in the internal cache. If necessary, introspection
    2213     * is performed now on the affected bean class, and the results object is created.
    2214     *
    2215     * @param beanClass the bean class in question
    2216     * @return the {@code BeanIntrospectionData} object for this class
    2217     * @throws IllegalArgumentException if the bean class is <b>null</b>
    2218     */
    2219    private BeanIntrospectionData getIntrospectionData(final Class<?> beanClass) {
    2220        if (beanClass == null) {
    2221            throw new IllegalArgumentException("No bean class specified");
    2222        }
    2223
    2224        // Look up any cached information for this bean class
    2225        BeanIntrospectionData data = descriptorsCache.get(beanClass);
    2226        if (data == null) {
    2227            data = fetchIntrospectionData(beanClass);
    2228            descriptorsCache.put(beanClass, data);
    2229        }
    2230
    2231        return data;
    2232    }
    2233
    2234    /**
    2235     * Performs introspection on the specified class. This method invokes all {@code BeanIntrospector} objects that were
    2236     * added to this instance.
    2237     *
    2238     * @param beanClass the class to be inspected
    2239     * @return a data object with the results of introspection
    2240     */
    2241    private BeanIntrospectionData fetchIntrospectionData(final Class<?> beanClass) {
    2242        final DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass);
    2243
    2244        for (final BeanIntrospector bi : introspectors) {
    2245            try {
    2246                bi.introspect(ictx);
    2247            } catch (final IntrospectionException iex) {
    2248                log.error("Exception during introspection", iex);
    2249            }
    2250        }
    2251
    2252        return new BeanIntrospectionData(ictx.getPropertyDescriptors());
    2253    }
    2254
    2255    /**
    2256     * Converts an object to a list of objects. This method is used when dealing
    2257     * with indexed properties. It assumes that indexed properties are stored as
    2258     * lists of objects.
    2259     *
    2260     * @param obj the object to be converted
    2261     * @return the resulting list of objects
    2262     */
    2263    private static List<Object> toObjectList(final Object obj) {
    2264        @SuppressWarnings("unchecked")
    2265        final
    2266        // indexed properties are stored in lists of objects
    2267        List<Object> list = (List<Object>) obj;
    2268        return list;
    2269    }
    2270
    2271    /**
    2272     * Converts an object to a map with property values. This method is used
    2273     * when dealing with mapped properties. It assumes that mapped properties
    2274     * are stored in a Map&lt;String, Object&gt;.
    2275     *
    2276     * @param obj the object to be converted
    2277     * @return the resulting properties map
    2278     */
    2279    private static Map<String, Object> toPropertyMap(final Object obj) {
    2280        @SuppressWarnings("unchecked")
    2281        final
    2282        // mapped properties are stores in maps of type <String, Object>
    2283        Map<String, Object> map = (Map<String, Object>) obj;
    2284        return map;
    2285    }
    2286}