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