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