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