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