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