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 1547898 2013-12-04 20:33:06Z 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<?>, PropertyDescriptor[]> 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<?>, PropertyDescriptor[]>();
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        PropertyDescriptor[] descriptors = getPropertyDescriptors(bean);
951        if (descriptors != null) {
952
953            for (int i = 0; i < descriptors.length; i++) {
954                if (name.equals(descriptors[i].getName())) {
955                    return (descriptors[i]);
956                }
957            }
958        }
959
960        PropertyDescriptor result = null;
961        FastHashMap mappedDescriptors =
962                getMappedPropertyDescriptors(bean);
963        if (mappedDescriptors == null) {
964            mappedDescriptors = new FastHashMap();
965            mappedDescriptors.setFast(true);
966            mappedDescriptorsCache.put(bean.getClass(), mappedDescriptors);
967        }
968        result = (PropertyDescriptor) mappedDescriptors.get(name);
969        if (result == null) {
970            // not found, try to create it
971            try {
972                result = new MappedPropertyDescriptor(name, bean.getClass());
973            } catch (IntrospectionException ie) {
974                /* Swallow IntrospectionException
975                 * TODO: Why?
976                 */
977            }
978            if (result != null) {
979                mappedDescriptors.put(name, result);
980            }
981        }
982
983        return result;
984
985    }
986
987
988    /**
989     * <p>Retrieve the property descriptors for the specified class,
990     * introspecting and caching them the first time a particular bean class
991     * is encountered.</p>
992     *
993     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
994     *
995     * @param beanClass Bean class for which property descriptors are requested
996     * @return the property descriptors
997     *
998     * @exception IllegalArgumentException if <code>beanClass</code> is null
999     */
1000    public PropertyDescriptor[]
1001            getPropertyDescriptors(Class<?> beanClass) {
1002
1003        if (beanClass == null) {
1004            throw new IllegalArgumentException("No bean class specified");
1005        }
1006
1007        // Look up any cached descriptors for this bean class
1008        PropertyDescriptor[] descriptors = null;
1009        descriptors =
1010                descriptorsCache.get(beanClass);
1011        if (descriptors != null) {
1012            return (descriptors);
1013        }
1014
1015        descriptors = fetchPropertyDescriptors(beanClass);
1016        descriptorsCache.put(beanClass, descriptors);
1017        return (descriptors);
1018
1019    }
1020
1021
1022    /**
1023     * <p>Retrieve the property descriptors for the specified bean,
1024     * introspecting and caching them the first time a particular bean class
1025     * is encountered.</p>
1026     *
1027     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
1028     *
1029     * @param bean Bean for which property descriptors are requested
1030     * @return the property descriptors
1031     *
1032     * @exception IllegalArgumentException if <code>bean</code> is null
1033     */
1034    public PropertyDescriptor[] getPropertyDescriptors(Object bean) {
1035
1036        if (bean == null) {
1037            throw new IllegalArgumentException("No bean specified");
1038        }
1039        return (getPropertyDescriptors(bean.getClass()));
1040
1041    }
1042
1043
1044    /**
1045     * <p>Return the Java Class repesenting the property editor class that has
1046     * been registered for this property (if any).  This method follows the
1047     * same name resolution rules used by <code>getPropertyDescriptor()</code>,
1048     * so if the last element of a name reference is indexed, the property
1049     * editor for the underlying property's class is returned.</p>
1050     *
1051     * <p>Note that <code>null</code> will be returned if there is no property,
1052     * or if there is no registered property editor class.  Because this
1053     * return value is ambiguous, you should determine the existence of the
1054     * property itself by other means.</p>
1055     *
1056     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
1057     *
1058     * @param bean Bean for which a property descriptor is requested
1059     * @param name Possibly indexed and/or nested name of the property for
1060     *  which a property descriptor is requested
1061     * @return the property editor class
1062     *
1063     * @exception IllegalAccessException if the caller does not have
1064     *  access to the property accessor method
1065     * @exception IllegalArgumentException if <code>bean</code> or
1066     *  <code>name</code> is null
1067     * @exception IllegalArgumentException if a nested reference to a
1068     *  property returns null
1069     * @exception InvocationTargetException if the property accessor method
1070     *  throws an exception
1071     * @exception NoSuchMethodException if an accessor method for this
1072     *  propety cannot be found
1073     */
1074    public Class<?> getPropertyEditorClass(Object bean, String name)
1075            throws IllegalAccessException, InvocationTargetException,
1076            NoSuchMethodException {
1077
1078        if (bean == null) {
1079            throw new IllegalArgumentException("No bean specified");
1080        }
1081        if (name == null) {
1082            throw new IllegalArgumentException("No name specified for bean class '" +
1083                    bean.getClass() + "'");
1084        }
1085
1086        PropertyDescriptor descriptor =
1087                getPropertyDescriptor(bean, name);
1088        if (descriptor != null) {
1089            return (descriptor.getPropertyEditorClass());
1090        } else {
1091            return (null);
1092        }
1093
1094    }
1095
1096
1097    /**
1098     * Return the Java Class representing the property type of the specified
1099     * property, or <code>null</code> if there is no such property for the
1100     * specified bean.  This method follows the same name resolution rules
1101     * used by <code>getPropertyDescriptor()</code>, so if the last element
1102     * of a name reference is indexed, the type of the property itself will
1103     * be returned.  If the last (or only) element has no property with the
1104     * specified name, <code>null</code> is returned.
1105     *
1106     * @param bean Bean for which a property descriptor is requested
1107     * @param name Possibly indexed and/or nested name of the property for
1108     *  which a property descriptor is requested
1109     * @return The property type
1110     *
1111     * @exception IllegalAccessException if the caller does not have
1112     *  access to the property accessor method
1113     * @exception IllegalArgumentException if <code>bean</code> or
1114     *  <code>name</code> is null
1115     * @exception IllegalArgumentException if a nested reference to a
1116     *  property returns null
1117     * @exception InvocationTargetException if the property accessor method
1118     *  throws an exception
1119     * @exception NoSuchMethodException if an accessor method for this
1120     *  propety cannot be found
1121     */
1122    public Class<?> getPropertyType(Object bean, String name)
1123            throws IllegalAccessException, InvocationTargetException,
1124            NoSuchMethodException {
1125
1126        if (bean == null) {
1127            throw new IllegalArgumentException("No bean specified");
1128        }
1129        if (name == null) {
1130            throw new IllegalArgumentException("No name specified for bean class '" +
1131                    bean.getClass() + "'");
1132        }
1133
1134        // Resolve nested references
1135        while (resolver.hasNested(name)) {
1136            String next = resolver.next(name);
1137            Object nestedBean = getProperty(bean, next);
1138            if (nestedBean == null) {
1139                throw new NestedNullException
1140                        ("Null property value for '" + next +
1141                        "' on bean class '" + bean.getClass() + "'");
1142            }
1143            bean = nestedBean;
1144            name = resolver.remove(name);
1145        }
1146
1147        // Remove any subscript from the final name value
1148        name = resolver.getProperty(name);
1149
1150        // Special handling for DynaBeans
1151        if (bean instanceof DynaBean) {
1152            DynaProperty descriptor =
1153                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1154            if (descriptor == null) {
1155                return (null);
1156            }
1157            Class<?> type = descriptor.getType();
1158            if (type == null) {
1159                return (null);
1160            } else if (type.isArray()) {
1161                return (type.getComponentType());
1162            } else {
1163                return (type);
1164            }
1165        }
1166
1167        PropertyDescriptor descriptor =
1168                getPropertyDescriptor(bean, name);
1169        if (descriptor == null) {
1170            return (null);
1171        } else if (descriptor instanceof IndexedPropertyDescriptor) {
1172            return (((IndexedPropertyDescriptor) descriptor).
1173                    getIndexedPropertyType());
1174        } else if (descriptor instanceof MappedPropertyDescriptor) {
1175            return (((MappedPropertyDescriptor) descriptor).
1176                    getMappedPropertyType());
1177        } else {
1178            return (descriptor.getPropertyType());
1179        }
1180
1181    }
1182
1183
1184    /**
1185     * <p>Return an accessible property getter method for this property,
1186     * if there is one; otherwise return <code>null</code>.</p>
1187     *
1188     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
1189     *
1190     * @param descriptor Property descriptor to return a getter for
1191     * @return The read method
1192     */
1193    public Method getReadMethod(PropertyDescriptor descriptor) {
1194
1195        return (MethodUtils.getAccessibleMethod(descriptor.getReadMethod()));
1196
1197    }
1198
1199
1200    /**
1201     * <p>Return an accessible property getter method for this property,
1202     * if there is one; otherwise return <code>null</code>.</p>
1203     *
1204     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
1205     *
1206     * @param clazz The class of the read method will be invoked on
1207     * @param descriptor Property descriptor to return a getter for
1208     * @return The read method
1209     */
1210    Method getReadMethod(Class<?> clazz, PropertyDescriptor descriptor) {
1211        return (MethodUtils.getAccessibleMethod(clazz, descriptor.getReadMethod()));
1212    }
1213
1214
1215    /**
1216     * Return the value of the specified simple property of the specified
1217     * bean, with no type conversions.
1218     *
1219     * @param bean Bean whose property is to be extracted
1220     * @param name Name of the property to be extracted
1221     * @return The property value
1222     *
1223     * @exception IllegalAccessException if the caller does not have
1224     *  access to the property accessor method
1225     * @exception IllegalArgumentException if <code>bean</code> or
1226     *  <code>name</code> is null
1227     * @exception IllegalArgumentException if the property name
1228     *  is nested or indexed
1229     * @exception InvocationTargetException if the property accessor method
1230     *  throws an exception
1231     * @exception NoSuchMethodException if an accessor method for this
1232     *  propety cannot be found
1233     */
1234    public Object getSimpleProperty(Object bean, String name)
1235            throws IllegalAccessException, InvocationTargetException,
1236            NoSuchMethodException {
1237
1238        if (bean == null) {
1239            throw new IllegalArgumentException("No bean specified");
1240        }
1241        if (name == null) {
1242            throw new IllegalArgumentException("No name specified for bean class '" +
1243                    bean.getClass() + "'");
1244        }
1245
1246        // Validate the syntax of the property name
1247        if (resolver.hasNested(name)) {
1248            throw new IllegalArgumentException
1249                    ("Nested property names are not allowed: Property '" +
1250                    name + "' on bean class '" + bean.getClass() + "'");
1251        } else if (resolver.isIndexed(name)) {
1252            throw new IllegalArgumentException
1253                    ("Indexed property names are not allowed: Property '" +
1254                    name + "' on bean class '" + bean.getClass() + "'");
1255        } else if (resolver.isMapped(name)) {
1256            throw new IllegalArgumentException
1257                    ("Mapped property names are not allowed: Property '" +
1258                    name + "' on bean class '" + bean.getClass() + "'");
1259        }
1260
1261        // Handle DynaBean instances specially
1262        if (bean instanceof DynaBean) {
1263            DynaProperty descriptor =
1264                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1265            if (descriptor == null) {
1266                throw new NoSuchMethodException("Unknown property '" +
1267                        name + "' on dynaclass '" +
1268                        ((DynaBean) bean).getDynaClass() + "'" );
1269            }
1270            return (((DynaBean) bean).get(name));
1271        }
1272
1273        // Retrieve the property getter method for the specified property
1274        PropertyDescriptor descriptor =
1275                getPropertyDescriptor(bean, name);
1276        if (descriptor == null) {
1277            throw new NoSuchMethodException("Unknown property '" +
1278                    name + "' on class '" + bean.getClass() + "'" );
1279        }
1280        Method readMethod = getReadMethod(bean.getClass(), descriptor);
1281        if (readMethod == null) {
1282            throw new NoSuchMethodException("Property '" + name +
1283                    "' has no getter method in class '" + bean.getClass() + "'");
1284        }
1285
1286        // Call the property getter and return the value
1287        Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
1288        return (value);
1289
1290    }
1291
1292
1293    /**
1294     * <p>Return an accessible property setter method for this property,
1295     * if there is one; otherwise return <code>null</code>.</p>
1296     *
1297     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
1298     *
1299     * @param descriptor Property descriptor to return a setter for
1300     * @return The write method
1301     */
1302    public Method getWriteMethod(PropertyDescriptor descriptor) {
1303
1304        return (MethodUtils.getAccessibleMethod(descriptor.getWriteMethod()));
1305
1306    }
1307
1308
1309    /**
1310     * <p>Return an accessible property setter method for this property,
1311     * if there is one; otherwise return <code>null</code>.</p>
1312     *
1313     * <p><strong>FIXME</strong> - Does not work with DynaBeans.</p>
1314     *
1315     * @param clazz The class of the read method will be invoked on
1316     * @param descriptor Property descriptor to return a setter for
1317     * @return The write method
1318     */
1319    Method getWriteMethod(Class<?> clazz, PropertyDescriptor descriptor) {
1320        return (MethodUtils.getAccessibleMethod(clazz, descriptor.getWriteMethod()));
1321    }
1322
1323
1324    /**
1325     * <p>Return <code>true</code> if the specified property name identifies
1326     * a readable property on the specified bean; otherwise, return
1327     * <code>false</code>.
1328     *
1329     * @param bean Bean to be examined (may be a {@link DynaBean}
1330     * @param name Property name to be evaluated
1331     * @return <code>true</code> if the property is readable,
1332     * otherwise <code>false</code>
1333     *
1334     * @exception IllegalArgumentException if <code>bean</code>
1335     *  or <code>name</code> is <code>null</code>
1336     *
1337     * @since BeanUtils 1.6
1338     */
1339    public boolean isReadable(Object bean, String name) {
1340
1341        // Validate method parameters
1342        if (bean == null) {
1343            throw new IllegalArgumentException("No bean specified");
1344        }
1345        if (name == null) {
1346            throw new IllegalArgumentException("No name specified for bean class '" +
1347                    bean.getClass() + "'");
1348        }
1349
1350        // Resolve nested references
1351        while (resolver.hasNested(name)) {
1352            String next = resolver.next(name);
1353            Object nestedBean = null;
1354            try {
1355                nestedBean = getProperty(bean, next);
1356            } catch (IllegalAccessException e) {
1357                return false;
1358            } catch (InvocationTargetException e) {
1359                return false;
1360            } catch (NoSuchMethodException e) {
1361                return false;
1362            }
1363            if (nestedBean == null) {
1364                throw new NestedNullException
1365                        ("Null property value for '" + next +
1366                        "' on bean class '" + bean.getClass() + "'");
1367            }
1368            bean = nestedBean;
1369            name = resolver.remove(name);
1370        }
1371
1372        // Remove any subscript from the final name value
1373        name = resolver.getProperty(name);
1374
1375        // Treat WrapDynaBean as special case - may be a write-only property
1376        // (see Jira issue# BEANUTILS-61)
1377        if (bean instanceof WrapDynaBean) {
1378            bean = ((WrapDynaBean)bean).getInstance();
1379        }
1380
1381        // Return the requested result
1382        if (bean instanceof DynaBean) {
1383            // All DynaBean properties are readable
1384            return (((DynaBean) bean).getDynaClass().getDynaProperty(name) != null);
1385        } else {
1386            try {
1387                PropertyDescriptor desc =
1388                    getPropertyDescriptor(bean, name);
1389                if (desc != null) {
1390                    Method readMethod = getReadMethod(bean.getClass(), desc);
1391                    if (readMethod == null) {
1392                        if (desc instanceof IndexedPropertyDescriptor) {
1393                            readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod();
1394                        } else if (desc instanceof MappedPropertyDescriptor) {
1395                            readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod();
1396                        }
1397                        readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
1398                    }
1399                    return (readMethod != null);
1400                } else {
1401                    return (false);
1402                }
1403            } catch (IllegalAccessException e) {
1404                return (false);
1405            } catch (InvocationTargetException e) {
1406                return (false);
1407            } catch (NoSuchMethodException e) {
1408                return (false);
1409            }
1410        }
1411
1412    }
1413
1414
1415    /**
1416     * <p>Return <code>true</code> if the specified property name identifies
1417     * a writeable property on the specified bean; otherwise, return
1418     * <code>false</code>.
1419     *
1420     * @param bean Bean to be examined (may be a {@link DynaBean}
1421     * @param name Property name to be evaluated
1422     * @return <code>true</code> if the property is writeable,
1423     * otherwise <code>false</code>
1424     *
1425     * @exception IllegalArgumentException if <code>bean</code>
1426     *  or <code>name</code> is <code>null</code>
1427     *
1428     * @since BeanUtils 1.6
1429     */
1430    public boolean isWriteable(Object bean, String name) {
1431
1432        // Validate method parameters
1433        if (bean == null) {
1434            throw new IllegalArgumentException("No bean specified");
1435        }
1436        if (name == null) {
1437            throw new IllegalArgumentException("No name specified for bean class '" +
1438                    bean.getClass() + "'");
1439        }
1440
1441        // Resolve nested references
1442        while (resolver.hasNested(name)) {
1443            String next = resolver.next(name);
1444            Object nestedBean = null;
1445            try {
1446                nestedBean = getProperty(bean, next);
1447            } catch (IllegalAccessException e) {
1448                return false;
1449            } catch (InvocationTargetException e) {
1450                return false;
1451            } catch (NoSuchMethodException e) {
1452                return false;
1453            }
1454            if (nestedBean == null) {
1455                throw new NestedNullException
1456                        ("Null property value for '" + next +
1457                        "' on bean class '" + bean.getClass() + "'");
1458            }
1459            bean = nestedBean;
1460            name = resolver.remove(name);
1461        }
1462
1463        // Remove any subscript from the final name value
1464        name = resolver.getProperty(name);
1465
1466        // Treat WrapDynaBean as special case - may be a read-only property
1467        // (see Jira issue# BEANUTILS-61)
1468        if (bean instanceof WrapDynaBean) {
1469            bean = ((WrapDynaBean)bean).getInstance();
1470        }
1471
1472        // Return the requested result
1473        if (bean instanceof DynaBean) {
1474            // All DynaBean properties are writeable
1475            return (((DynaBean) bean).getDynaClass().getDynaProperty(name) != null);
1476        } else {
1477            try {
1478                PropertyDescriptor desc =
1479                    getPropertyDescriptor(bean, name);
1480                if (desc != null) {
1481                    Method writeMethod = getWriteMethod(bean.getClass(), desc);
1482                    if (writeMethod == null) {
1483                        if (desc instanceof IndexedPropertyDescriptor) {
1484                            writeMethod = ((IndexedPropertyDescriptor) desc).getIndexedWriteMethod();
1485                        } else if (desc instanceof MappedPropertyDescriptor) {
1486                            writeMethod = ((MappedPropertyDescriptor) desc).getMappedWriteMethod();
1487                        }
1488                        writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
1489                    }
1490                    return (writeMethod != null);
1491                } else {
1492                    return (false);
1493                }
1494            } catch (IllegalAccessException e) {
1495                return (false);
1496            } catch (InvocationTargetException e) {
1497                return (false);
1498            } catch (NoSuchMethodException e) {
1499                return (false);
1500            }
1501        }
1502
1503    }
1504
1505
1506    /**
1507     * Set the value of the specified indexed property of the specified
1508     * bean, with no type conversions.  The zero-relative index of the
1509     * required value must be included (in square brackets) as a suffix to
1510     * the property name, or <code>IllegalArgumentException</code> will be
1511     * thrown.  In addition to supporting the JavaBeans specification, this
1512     * method has been extended to support <code>List</code> objects as well.
1513     *
1514     * @param bean Bean whose property is to be modified
1515     * @param name <code>propertyname[index]</code> of the property value
1516     *  to be modified
1517     * @param value Value to which the specified property element
1518     *  should be set
1519     *
1520     * @exception IndexOutOfBoundsException if the specified index
1521     *  is outside the valid range for the underlying property
1522     * @exception IllegalAccessException if the caller does not have
1523     *  access to the property accessor method
1524     * @exception IllegalArgumentException if <code>bean</code> or
1525     *  <code>name</code> is null
1526     * @exception InvocationTargetException if the property accessor method
1527     *  throws an exception
1528     * @exception NoSuchMethodException if an accessor method for this
1529     *  propety cannot be found
1530     */
1531    public void setIndexedProperty(Object bean, String name,
1532                                          Object value)
1533            throws IllegalAccessException, InvocationTargetException,
1534            NoSuchMethodException {
1535
1536        if (bean == null) {
1537            throw new IllegalArgumentException("No bean specified");
1538        }
1539        if (name == null) {
1540            throw new IllegalArgumentException("No name specified for bean class '" +
1541                    bean.getClass() + "'");
1542        }
1543
1544        // Identify the index of the requested individual property
1545        int index = -1;
1546        try {
1547            index = resolver.getIndex(name);
1548        } catch (IllegalArgumentException e) {
1549            throw new IllegalArgumentException("Invalid indexed property '" +
1550                    name + "' on bean class '" + bean.getClass() + "'");
1551        }
1552        if (index < 0) {
1553            throw new IllegalArgumentException("Invalid indexed property '" +
1554                    name + "' on bean class '" + bean.getClass() + "'");
1555        }
1556
1557        // Isolate the name
1558        name = resolver.getProperty(name);
1559
1560        // Set the specified indexed property value
1561        setIndexedProperty(bean, name, index, value);
1562
1563    }
1564
1565
1566    /**
1567     * Set the value of the specified indexed property of the specified
1568     * bean, with no type conversions.  In addition to supporting the JavaBeans
1569     * specification, this method has been extended to support
1570     * <code>List</code> objects as well.
1571     *
1572     * @param bean Bean whose property is to be set
1573     * @param name Simple property name of the property value to be set
1574     * @param index Index of the property value to be set
1575     * @param value Value to which the indexed property element is to be set
1576     *
1577     * @exception IndexOutOfBoundsException if the specified index
1578     *  is outside the valid range for the underlying property
1579     * @exception IllegalAccessException if the caller does not have
1580     *  access to the property accessor method
1581     * @exception IllegalArgumentException if <code>bean</code> or
1582     *  <code>name</code> is null
1583     * @exception InvocationTargetException if the property accessor method
1584     *  throws an exception
1585     * @exception NoSuchMethodException if an accessor method for this
1586     *  propety cannot be found
1587     */
1588    public void setIndexedProperty(Object bean, String name,
1589                                          int index, Object value)
1590            throws IllegalAccessException, InvocationTargetException,
1591            NoSuchMethodException {
1592
1593        if (bean == null) {
1594            throw new IllegalArgumentException("No bean specified");
1595        }
1596        if (name == null || name.length() == 0) {
1597            if (bean.getClass().isArray()) {
1598                Array.set(bean, index, value);
1599                return;
1600            } else if (bean instanceof List) {
1601                List<Object> list = toObjectList(bean);
1602                list.set(index, value);
1603                return;
1604            }
1605        }
1606        if (name == null) {
1607            throw new IllegalArgumentException("No name specified for bean class '" +
1608                    bean.getClass() + "'");
1609        }
1610
1611        // Handle DynaBean instances specially
1612        if (bean instanceof DynaBean) {
1613            DynaProperty descriptor =
1614                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1615            if (descriptor == null) {
1616                throw new NoSuchMethodException("Unknown property '" +
1617                        name + "' on bean class '" + bean.getClass() + "'");
1618            }
1619            ((DynaBean) bean).set(name, index, value);
1620            return;
1621        }
1622
1623        // Retrieve the property descriptor for the specified property
1624        PropertyDescriptor descriptor =
1625                getPropertyDescriptor(bean, name);
1626        if (descriptor == null) {
1627            throw new NoSuchMethodException("Unknown property '" +
1628                    name + "' on bean class '" + bean.getClass() + "'");
1629        }
1630
1631        // Call the indexed setter method if there is one
1632        if (descriptor instanceof IndexedPropertyDescriptor) {
1633            Method writeMethod = ((IndexedPropertyDescriptor) descriptor).
1634                    getIndexedWriteMethod();
1635            writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
1636            if (writeMethod != null) {
1637                Object[] subscript = new Object[2];
1638                subscript[0] = new Integer(index);
1639                subscript[1] = value;
1640                try {
1641                    if (log.isTraceEnabled()) {
1642                        String valueClassName =
1643                            value == null ? "<null>"
1644                                          : value.getClass().getName();
1645                        log.trace("setSimpleProperty: Invoking method "
1646                                  + writeMethod +" with index=" + index
1647                                  + ", value=" + value
1648                                  + " (class " + valueClassName+ ")");
1649                    }
1650                    invokeMethod(writeMethod, bean, subscript);
1651                } catch (InvocationTargetException e) {
1652                    if (e.getTargetException() instanceof
1653                            IndexOutOfBoundsException) {
1654                        throw (IndexOutOfBoundsException)
1655                                e.getTargetException();
1656                    } else {
1657                        throw e;
1658                    }
1659                }
1660                return;
1661            }
1662        }
1663
1664        // Otherwise, the underlying property must be an array or a list
1665        Method readMethod = getReadMethod(bean.getClass(), descriptor);
1666        if (readMethod == null) {
1667            throw new NoSuchMethodException("Property '" + name +
1668                    "' has no getter method on bean class '" + bean.getClass() + "'");
1669        }
1670
1671        // Call the property getter to get the array or list
1672        Object array = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
1673        if (!array.getClass().isArray()) {
1674            if (array instanceof List) {
1675                // Modify the specified value in the List
1676                List<Object> list = toObjectList(array);
1677                list.set(index, value);
1678            } else {
1679                throw new IllegalArgumentException("Property '" + name +
1680                        "' is not indexed on bean class '" + bean.getClass() + "'");
1681            }
1682        } else {
1683            // Modify the specified value in the array
1684            Array.set(array, index, value);
1685        }
1686
1687    }
1688
1689
1690    /**
1691     * Set the value of the specified mapped property of the
1692     * specified bean, with no type conversions.  The key of the
1693     * value to set must be included (in brackets) as a suffix to
1694     * the property name, or <code>IllegalArgumentException</code> will be
1695     * thrown.
1696     *
1697     * @param bean Bean whose property is to be set
1698     * @param name <code>propertyname(key)</code> of the property value
1699     *  to be set
1700     * @param value The property value to be set
1701     *
1702     * @exception IllegalAccessException if the caller does not have
1703     *  access to the property accessor method
1704     * @exception InvocationTargetException if the property accessor method
1705     *  throws an exception
1706     * @exception NoSuchMethodException if an accessor method for this
1707     *  propety cannot be found
1708     */
1709    public void setMappedProperty(Object bean, String name,
1710                                         Object value)
1711            throws IllegalAccessException, InvocationTargetException,
1712            NoSuchMethodException {
1713
1714        if (bean == null) {
1715            throw new IllegalArgumentException("No bean specified");
1716        }
1717        if (name == null) {
1718            throw new IllegalArgumentException("No name specified for bean class '" +
1719                    bean.getClass() + "'");
1720        }
1721
1722        // Identify the key of the requested individual property
1723        String key  = null;
1724        try {
1725            key = resolver.getKey(name);
1726        } catch (IllegalArgumentException e) {
1727            throw new IllegalArgumentException
1728                    ("Invalid mapped property '" + name +
1729                    "' on bean class '" + bean.getClass() + "'");
1730        }
1731        if (key == null) {
1732            throw new IllegalArgumentException
1733                    ("Invalid mapped property '" + name +
1734                    "' on bean class '" + bean.getClass() + "'");
1735        }
1736
1737        // Isolate the name
1738        name = resolver.getProperty(name);
1739
1740        // Request the specified indexed property value
1741        setMappedProperty(bean, name, key, value);
1742
1743    }
1744
1745
1746    /**
1747     * Set the value of the specified mapped property of the specified
1748     * bean, with no type conversions.
1749     *
1750     * @param bean Bean whose property is to be set
1751     * @param name Mapped property name of the property value to be set
1752     * @param key Key of the property value to be set
1753     * @param value The property value to be set
1754     *
1755     * @exception IllegalAccessException if the caller does not have
1756     *  access to the property accessor method
1757     * @exception InvocationTargetException if the property accessor method
1758     *  throws an exception
1759     * @exception NoSuchMethodException if an accessor method for this
1760     *  propety cannot be found
1761     */
1762    public void setMappedProperty(Object bean, String name,
1763                                         String key, Object value)
1764            throws IllegalAccessException, InvocationTargetException,
1765            NoSuchMethodException {
1766
1767        if (bean == null) {
1768            throw new IllegalArgumentException("No bean specified");
1769        }
1770        if (name == null) {
1771            throw new IllegalArgumentException("No name specified for bean class '" +
1772                    bean.getClass() + "'");
1773        }
1774        if (key == null) {
1775            throw new IllegalArgumentException("No key specified for property '" +
1776                    name + "' on bean class '" + bean.getClass() + "'");
1777        }
1778
1779        // Handle DynaBean instances specially
1780        if (bean instanceof DynaBean) {
1781            DynaProperty descriptor =
1782                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1783            if (descriptor == null) {
1784                throw new NoSuchMethodException("Unknown property '" +
1785                        name + "' on bean class '" + bean.getClass() + "'");
1786            }
1787            ((DynaBean) bean).set(name, key, value);
1788            return;
1789        }
1790
1791        // Retrieve the property descriptor for the specified property
1792        PropertyDescriptor descriptor =
1793                getPropertyDescriptor(bean, name);
1794        if (descriptor == null) {
1795            throw new NoSuchMethodException("Unknown property '" +
1796                    name + "' on bean class '" + bean.getClass() + "'");
1797        }
1798
1799        if (descriptor instanceof MappedPropertyDescriptor) {
1800            // Call the keyed setter method if there is one
1801            Method mappedWriteMethod =
1802                    ((MappedPropertyDescriptor) descriptor).
1803                    getMappedWriteMethod();
1804            mappedWriteMethod = MethodUtils.getAccessibleMethod(bean.getClass(), mappedWriteMethod);
1805            if (mappedWriteMethod != null) {
1806                Object[] params = new Object[2];
1807                params[0] = key;
1808                params[1] = value;
1809                if (log.isTraceEnabled()) {
1810                    String valueClassName =
1811                        value == null ? "<null>" : value.getClass().getName();
1812                    log.trace("setSimpleProperty: Invoking method "
1813                              + mappedWriteMethod + " with key=" + key
1814                              + ", value=" + value
1815                              + " (class " + valueClassName +")");
1816                }
1817                invokeMethod(mappedWriteMethod, bean, params);
1818            } else {
1819                throw new NoSuchMethodException
1820                    ("Property '" + name + "' has no mapped setter method" +
1821                     "on bean class '" + bean.getClass() + "'");
1822            }
1823        } else {
1824          /* means that the result has to be retrieved from a map */
1825          Method readMethod = getReadMethod(bean.getClass(), descriptor);
1826          if (readMethod != null) {
1827            Object invokeResult = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY);
1828            /* test and fetch from the map */
1829            if (invokeResult instanceof java.util.Map) {
1830              java.util.Map<String, Object> map = toPropertyMap(invokeResult);
1831              map.put(key, value);
1832            }
1833          } else {
1834            throw new NoSuchMethodException("Property '" + name +
1835                    "' has no mapped getter method on bean class '" +
1836                    bean.getClass() + "'");
1837          }
1838        }
1839
1840    }
1841
1842
1843    /**
1844     * Set the value of the (possibly nested) property of the specified
1845     * name, for the specified bean, with no type conversions.
1846     * <p>
1847     * Example values for parameter "name" are:
1848     * <ul>
1849     * <li> "a" -- sets the value of property a of the specified bean </li>
1850     * <li> "a.b" -- gets the value of property a of the specified bean,
1851     * then on that object sets the value of property b.</li>
1852     * <li> "a(key)" -- sets a value of mapped-property a on the specified
1853     * bean. This effectively means bean.setA("key").</li>
1854     * <li> "a[3]" -- sets a value of indexed-property a on the specified
1855     * bean. This effectively means bean.setA(3).</li>
1856     * </ul>
1857     *
1858     * @param bean Bean whose property is to be modified
1859     * @param name Possibly nested name of the property to be modified
1860     * @param value Value to which the property is to be set
1861     *
1862     * @exception IllegalAccessException if the caller does not have
1863     *  access to the property accessor method
1864     * @exception IllegalArgumentException if <code>bean</code> or
1865     *  <code>name</code> is null
1866     * @exception IllegalArgumentException if a nested reference to a
1867     *  property returns null
1868     * @exception InvocationTargetException if the property accessor method
1869     *  throws an exception
1870     * @exception NoSuchMethodException if an accessor method for this
1871     *  propety cannot be found
1872     */
1873    public void setNestedProperty(Object bean,
1874                                         String name, Object value)
1875            throws IllegalAccessException, InvocationTargetException,
1876            NoSuchMethodException {
1877
1878        if (bean == null) {
1879            throw new IllegalArgumentException("No bean specified");
1880        }
1881        if (name == null) {
1882            throw new IllegalArgumentException("No name specified for bean class '" +
1883                    bean.getClass() + "'");
1884        }
1885
1886        // Resolve nested references
1887        while (resolver.hasNested(name)) {
1888            String next = resolver.next(name);
1889            Object nestedBean = null;
1890            if (bean instanceof Map) {
1891                nestedBean = getPropertyOfMapBean((Map<?, ?>)bean, next);
1892            } else if (resolver.isMapped(next)) {
1893                nestedBean = getMappedProperty(bean, next);
1894            } else if (resolver.isIndexed(next)) {
1895                nestedBean = getIndexedProperty(bean, next);
1896            } else {
1897                nestedBean = getSimpleProperty(bean, next);
1898            }
1899            if (nestedBean == null) {
1900                throw new NestedNullException
1901                        ("Null property value for '" + name +
1902                         "' on bean class '" + bean.getClass() + "'");
1903            }
1904            bean = nestedBean;
1905            name = resolver.remove(name);
1906        }
1907
1908        if (bean instanceof Map) {
1909            setPropertyOfMapBean(toPropertyMap(bean), name, value);
1910        } else if (resolver.isMapped(name)) {
1911            setMappedProperty(bean, name, value);
1912        } else if (resolver.isIndexed(name)) {
1913            setIndexedProperty(bean, name, value);
1914        } else {
1915            setSimpleProperty(bean, name, value);
1916        }
1917
1918    }
1919
1920    /**
1921     * This method is called by method setNestedProperty when the current bean
1922     * is found to be a Map object, and defines how to deal with setting
1923     * a property on a Map.
1924     * <p>
1925     * The standard implementation here is to:
1926     * <ul>
1927     * <li>call bean.set(propertyName) for all propertyName values.</li>
1928     * <li>throw an IllegalArgumentException if the property specifier
1929     * contains MAPPED_DELIM or INDEXED_DELIM, as Map entries are essentially
1930     * simple properties; mapping and indexing operations do not make sense
1931     * when accessing a map (even thought the returned object may be a Map
1932     * or an Array).</li>
1933     * </ul>
1934     * <p>
1935     * The default behaviour of beanutils 1.7.1 or later is for assigning to
1936     * "a.b" to mean a.put(b, obj) always. However the behaviour of beanutils
1937     * version 1.6.0, 1.6.1, 1.7.0 was for "a.b" to mean a.setB(obj) if such
1938     * a method existed, and a.put(b, obj) otherwise. In version 1.5 it meant
1939     * a.put(b, obj) always (ie the same as the behaviour in the current version).
1940     * In versions prior to 1.5 it meant a.setB(obj) always. [yes, this is
1941     * all <i>very</i> unfortunate]
1942     * <p>
1943     * Users who would like to customise the meaning of "a.b" in method
1944     * setNestedProperty when a is a Map can create a custom subclass of
1945     * this class and override this method to implement the behaviour of
1946     * their choice, such as restoring the pre-1.4 behaviour of this class
1947     * if they wish. When overriding this method, do not forget to deal
1948     * with MAPPED_DELIM and INDEXED_DELIM characters in the propertyName.
1949     * <p>
1950     * Note, however, that the recommended solution for objects that
1951     * implement Map but want their simple properties to come first is
1952     * for <i>those</i> objects to override their get/put methods to implement
1953     * that behaviour, and <i>not</i> to solve the problem by modifying the
1954     * default behaviour of the PropertyUtilsBean class by overriding this
1955     * method.
1956     *
1957     * @param bean Map bean
1958     * @param propertyName The property name
1959     * @param value the property value
1960     *
1961     * @throws IllegalArgumentException when the propertyName is regarded as
1962     * being invalid.
1963     *
1964     * @throws IllegalAccessException just in case subclasses override this
1965     * method to try to access real setter methods and find permission is denied.
1966     *
1967     * @throws InvocationTargetException just in case subclasses override this
1968     * method to try to access real setter methods, and find it throws an
1969     * exception when invoked.
1970     *
1971     * @throws NoSuchMethodException just in case subclasses override this
1972     * method to try to access real setter methods, and want to fail if
1973     * no simple method is available.
1974     * @since 1.8.0
1975     */
1976    protected void setPropertyOfMapBean(Map<String, Object> bean, String propertyName, Object value)
1977        throws IllegalArgumentException, IllegalAccessException,
1978        InvocationTargetException, NoSuchMethodException {
1979
1980        if (resolver.isMapped(propertyName)) {
1981            String name = resolver.getProperty(propertyName);
1982            if (name == null || name.length() == 0) {
1983                propertyName = resolver.getKey(propertyName);
1984            }
1985        }
1986
1987        if (resolver.isIndexed(propertyName) ||
1988            resolver.isMapped(propertyName)) {
1989            throw new IllegalArgumentException(
1990                    "Indexed or mapped properties are not supported on"
1991                    + " objects of type Map: " + propertyName);
1992        }
1993
1994        bean.put(propertyName, value);
1995    }
1996
1997
1998
1999    /**
2000     * Set the value of the specified property of the specified bean,
2001     * no matter which property reference format is used, with no
2002     * type conversions.
2003     *
2004     * @param bean Bean whose property is to be modified
2005     * @param name Possibly indexed and/or nested name of the property
2006     *  to be modified
2007     * @param value Value to which this property is to be set
2008     *
2009     * @exception IllegalAccessException if the caller does not have
2010     *  access to the property accessor method
2011     * @exception IllegalArgumentException if <code>bean</code> or
2012     *  <code>name</code> is null
2013     * @exception InvocationTargetException if the property accessor method
2014     *  throws an exception
2015     * @exception NoSuchMethodException if an accessor method for this
2016     *  propety cannot be found
2017     */
2018    public void setProperty(Object bean, String name, Object value)
2019            throws IllegalAccessException, InvocationTargetException,
2020            NoSuchMethodException {
2021
2022        setNestedProperty(bean, name, value);
2023
2024    }
2025
2026
2027    /**
2028     * Set the value of the specified simple property of the specified bean,
2029     * with no type conversions.
2030     *
2031     * @param bean Bean whose property is to be modified
2032     * @param name Name of the property to be modified
2033     * @param value Value to which the property should be set
2034     *
2035     * @exception IllegalAccessException if the caller does not have
2036     *  access to the property accessor method
2037     * @exception IllegalArgumentException if <code>bean</code> or
2038     *  <code>name</code> is null
2039     * @exception IllegalArgumentException if the property name is
2040     *  nested or indexed
2041     * @exception InvocationTargetException if the property accessor method
2042     *  throws an exception
2043     * @exception NoSuchMethodException if an accessor method for this
2044     *  propety cannot be found
2045     */
2046    public void setSimpleProperty(Object bean,
2047                                         String name, Object value)
2048            throws IllegalAccessException, InvocationTargetException,
2049            NoSuchMethodException {
2050
2051        if (bean == null) {
2052            throw new IllegalArgumentException("No bean specified");
2053        }
2054        if (name == null) {
2055            throw new IllegalArgumentException("No name specified for bean class '" +
2056                    bean.getClass() + "'");
2057        }
2058
2059        // Validate the syntax of the property name
2060        if (resolver.hasNested(name)) {
2061            throw new IllegalArgumentException
2062                    ("Nested property names are not allowed: Property '" +
2063                    name + "' on bean class '" + bean.getClass() + "'");
2064        } else if (resolver.isIndexed(name)) {
2065            throw new IllegalArgumentException
2066                    ("Indexed property names are not allowed: Property '" +
2067                    name + "' on bean class '" + bean.getClass() + "'");
2068        } else if (resolver.isMapped(name)) {
2069            throw new IllegalArgumentException
2070                    ("Mapped property names are not allowed: Property '" +
2071                    name + "' on bean class '" + bean.getClass() + "'");
2072        }
2073
2074        // Handle DynaBean instances specially
2075        if (bean instanceof DynaBean) {
2076            DynaProperty descriptor =
2077                    ((DynaBean) bean).getDynaClass().getDynaProperty(name);
2078            if (descriptor == null) {
2079                throw new NoSuchMethodException("Unknown property '" +
2080                        name + "' on dynaclass '" +
2081                        ((DynaBean) bean).getDynaClass() + "'" );
2082            }
2083            ((DynaBean) bean).set(name, value);
2084            return;
2085        }
2086
2087        // Retrieve the property setter method for the specified property
2088        PropertyDescriptor descriptor =
2089                getPropertyDescriptor(bean, name);
2090        if (descriptor == null) {
2091            throw new NoSuchMethodException("Unknown property '" +
2092                    name + "' on class '" + bean.getClass() + "'" );
2093        }
2094        Method writeMethod = getWriteMethod(bean.getClass(), descriptor);
2095        if (writeMethod == null) {
2096            throw new NoSuchMethodException("Property '" + name +
2097                    "' has no setter method in class '" + bean.getClass() + "'");
2098        }
2099
2100        // Call the property setter method
2101        Object[] values = new Object[1];
2102        values[0] = value;
2103        if (log.isTraceEnabled()) {
2104            String valueClassName =
2105                value == null ? "<null>" : value.getClass().getName();
2106            log.trace("setSimpleProperty: Invoking method " + writeMethod
2107                      + " with value " + value + " (class " + valueClassName + ")");
2108        }
2109        invokeMethod(writeMethod, bean, values);
2110
2111    }
2112
2113    /** This just catches and wraps IllegalArgumentException. */
2114    private Object invokeMethod(
2115                        Method method,
2116                        Object bean,
2117                        Object[] values)
2118                            throws
2119                                IllegalAccessException,
2120                                InvocationTargetException {
2121        if(bean == null) {
2122            throw new IllegalArgumentException("No bean specified " +
2123                "- this should have been checked before reaching this method");
2124        }
2125
2126        try {
2127
2128            return method.invoke(bean, values);
2129
2130        } catch (NullPointerException cause) {
2131            // JDK 1.3 and JDK 1.4 throw NullPointerException if an argument is
2132            // null for a primitive value (JDK 1.5+ throw IllegalArgumentException)
2133            String valueString = "";
2134            if (values != null) {
2135                for (int i = 0; i < values.length; i++) {
2136                    if (i>0) {
2137                        valueString += ", " ;
2138                    }
2139                    if (values[i] == null) {
2140                        valueString += "<null>";
2141                    } else {
2142                        valueString += (values[i]).getClass().getName();
2143                    }
2144                }
2145            }
2146            String expectedString = "";
2147            Class<?>[] parTypes = method.getParameterTypes();
2148            if (parTypes != null) {
2149                for (int i = 0; i < parTypes.length; i++) {
2150                    if (i > 0) {
2151                        expectedString += ", ";
2152                    }
2153                    expectedString += parTypes[i].getName();
2154                }
2155            }
2156            IllegalArgumentException e = new IllegalArgumentException(
2157                "Cannot invoke " + method.getDeclaringClass().getName() + "."
2158                + method.getName() + " on bean class '" + bean.getClass() +
2159                "' - " + cause.getMessage()
2160                // as per https://issues.apache.org/jira/browse/BEANUTILS-224
2161                + " - had objects of type \"" + valueString
2162                + "\" but expected signature \""
2163                +   expectedString + "\""
2164                );
2165            if (!BeanUtils.initCause(e, cause)) {
2166                log.error("Method invocation failed", cause);
2167            }
2168            throw e;
2169        } catch (IllegalArgumentException cause) {
2170            String valueString = "";
2171            if (values != null) {
2172                for (int i = 0; i < values.length; i++) {
2173                    if (i>0) {
2174                        valueString += ", " ;
2175                    }
2176                    if (values[i] == null) {
2177                        valueString += "<null>";
2178                    } else {
2179                        valueString += (values[i]).getClass().getName();
2180                    }
2181                }
2182            }
2183            String expectedString = "";
2184            Class<?>[] parTypes = method.getParameterTypes();
2185            if (parTypes != null) {
2186                for (int i = 0; i < parTypes.length; i++) {
2187                    if (i > 0) {
2188                        expectedString += ", ";
2189                    }
2190                    expectedString += parTypes[i].getName();
2191                }
2192            }
2193            IllegalArgumentException e = new IllegalArgumentException(
2194                "Cannot invoke " + method.getDeclaringClass().getName() + "."
2195                + method.getName() + " on bean class '" + bean.getClass() +
2196                "' - " + cause.getMessage()
2197                // as per https://issues.apache.org/jira/browse/BEANUTILS-224
2198                + " - had objects of type \"" + valueString
2199                + "\" but expected signature \""
2200                +   expectedString + "\""
2201                );
2202            if (!BeanUtils.initCause(e, cause)) {
2203                log.error("Method invocation failed", cause);
2204            }
2205            throw e;
2206
2207        }
2208    }
2209
2210    /**
2211     * Performs introspection on the specified class. This method invokes all {@code BeanIntrospector} objects that were
2212     * added to this instance.
2213     *
2214     * @param beanClass the class to be inspected
2215     * @return an array with all property descriptors found
2216     */
2217    private PropertyDescriptor[] fetchPropertyDescriptors(Class<?> beanClass) {
2218        DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass);
2219
2220        for (BeanIntrospector bi : introspectors) {
2221            try {
2222                bi.introspect(ictx);
2223            } catch (IntrospectionException iex) {
2224                log.error("Exception during introspection", iex);
2225            }
2226        }
2227
2228        return ictx.getPropertyDescriptors();
2229    }
2230
2231    /**
2232     * Converts an object to a list of objects. This method is used when dealing
2233     * with indexed properties. It assumes that indexed properties are stored as
2234     * lists of objects.
2235     *
2236     * @param obj the object to be converted
2237     * @return the resulting list of objects
2238     */
2239    private static List<Object> toObjectList(Object obj) {
2240        @SuppressWarnings("unchecked")
2241        // indexed properties are stored in lists of objects
2242        List<Object> list = (List<Object>) obj;
2243        return list;
2244    }
2245
2246    /**
2247     * Converts an object to a map with property values. This method is used
2248     * when dealing with mapped properties. It assumes that mapped properties
2249     * are stored in a Map&lt;String, Object&gt;.
2250     *
2251     * @param obj the object to be converted
2252     * @return the resulting properties map
2253     */
2254    private static Map<String, Object> toPropertyMap(Object obj) {
2255        @SuppressWarnings("unchecked")
2256        // mapped properties are stores in maps of type <String, Object>
2257        Map<String, Object> map = (Map<String, Object>) obj;
2258        return map;
2259    }
2260}