View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  
19  package org.apache.commons.beanutils;
20  
21  
22  import java.beans.IndexedPropertyDescriptor;
23  import java.beans.PropertyDescriptor;
24  import java.lang.reflect.Array;
25  import java.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Method;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  
33  import org.apache.commons.beanutils.expression.Resolver;
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  
37  
38  /**
39   * <p>JavaBean property population methods.</p>
40   *
41   * <p>This class provides implementations for the utility methods in
42   * {@link BeanUtils}.
43   * Different instances can be used to isolate caches between classloaders
44   * and to vary the value converters registered.</p>
45   *
46   * @version $Id$
47   * @see BeanUtils
48   * @since 1.7
49   */
50  
51  public class BeanUtilsBean {
52  
53  
54      // ------------------------------------------------------ Private Class Variables
55  
56      /**
57       * Contains <code>BeanUtilsBean</code> instances indexed by context classloader.
58       */
59      private static final ContextClassLoaderLocal<BeanUtilsBean>
60              BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
61                          // Creates the default instance used when the context classloader is unavailable
62                          @Override
63                          protected BeanUtilsBean initialValue() {
64                              return new BeanUtilsBean();
65                          }
66                      };
67  
68      /**
69       * Gets the instance which provides the functionality for {@link BeanUtils}.
70       * This is a pseudo-singleton - an single instance is provided per (thread) context classloader.
71       * This mechanism provides isolation for web apps deployed in the same container.
72       *
73       * @return The (pseudo-singleton) BeanUtils bean instance
74       */
75      public static BeanUtilsBean getInstance() {
76          return BEANS_BY_CLASSLOADER.get();
77      }
78  
79      /**
80       * Sets the instance which provides the functionality for {@link BeanUtils}.
81       * This is a pseudo-singleton - an single instance is provided per (thread) context classloader.
82       * This mechanism provides isolation for web apps deployed in the same container.
83       *
84       * @param newInstance The (pseudo-singleton) BeanUtils bean instance
85       */
86      public static void setInstance(final BeanUtilsBean newInstance) {
87          BEANS_BY_CLASSLOADER.set(newInstance);
88      }
89  
90      // --------------------------------------------------------- Attributes
91  
92      /**
93       * Logging for this instance
94       */
95      private final Log log = LogFactory.getLog(BeanUtils.class);
96  
97      /** Used to perform conversions between object types when setting properties */
98      private final ConvertUtilsBean convertUtilsBean;
99  
100     /** Used to access properties*/
101     private final PropertyUtilsBean propertyUtilsBean;
102 
103     /** A reference to Throwable's initCause method, or null if it's not there in this JVM */
104     private static final Method INIT_CAUSE_METHOD = getInitCauseMethod();
105 
106     // --------------------------------------------------------- Constuctors
107 
108     /**
109      * <p>Constructs an instance using new property
110      * and conversion instances.</p>
111      */
112     public BeanUtilsBean() {
113         this(new ConvertUtilsBean(), new PropertyUtilsBean());
114     }
115 
116     /**
117      * <p>Constructs an instance using given conversion instances
118      * and new {@link PropertyUtilsBean} instance.</p>
119      *
120      * @param convertUtilsBean use this <code>ConvertUtilsBean</code>
121      * to perform conversions from one object to another
122      *
123      * @since 1.8.0
124      */
125     public BeanUtilsBean(final ConvertUtilsBean convertUtilsBean) {
126         this(convertUtilsBean, new PropertyUtilsBean());
127     }
128 
129     /**
130      * <p>Constructs an instance using given property and conversion instances.</p>
131      *
132      * @param convertUtilsBean use this <code>ConvertUtilsBean</code>
133      * to perform conversions from one object to another
134      * @param propertyUtilsBean use this <code>PropertyUtilsBean</code>
135      * to access properties
136      */
137     public BeanUtilsBean(
138                             final ConvertUtilsBean convertUtilsBean,
139                             final PropertyUtilsBean propertyUtilsBean) {
140 
141         this.convertUtilsBean = convertUtilsBean;
142         this.propertyUtilsBean = propertyUtilsBean;
143     }
144 
145     // --------------------------------------------------------- Public Methods
146 
147     /**
148      * <p>Clone a bean based on the available property getters and setters,
149      * even if the bean class itself does not implement Cloneable.</p>
150      *
151      * <p>
152      * <strong>Note:</strong> this method creates a <strong>shallow</strong> clone.
153      * In other words, any objects referred to by the bean are shared with the clone
154      * rather than being cloned in turn.
155      * </p>
156      *
157      * @param bean Bean to be cloned
158      * @return the cloned bean
159      *
160      * @throws IllegalAccessException if the caller does not have
161      *  access to the property accessor method
162      * @throws InstantiationException if a new instance of the bean's
163      *  class cannot be instantiated
164      * @throws InvocationTargetException if the property accessor method
165      *  throws an exception
166      * @throws NoSuchMethodException if an accessor method for this
167      *  property cannot be found
168      */
169     public Object cloneBean(final Object bean)
170             throws IllegalAccessException, InstantiationException,
171             InvocationTargetException, NoSuchMethodException {
172 
173         if (log.isDebugEnabled()) {
174             log.debug("Cloning bean: " + bean.getClass().getName());
175         }
176         Object newBean = null;
177         if (bean instanceof DynaBean) {
178             newBean = ((DynaBean) bean).getDynaClass().newInstance();
179         } else {
180             newBean = bean.getClass().newInstance();
181         }
182         getPropertyUtils().copyProperties(newBean, bean);
183         return (newBean);
184 
185     }
186 
187 
188     /**
189      * <p>Copy property values from the origin bean to the destination bean
190      * for all cases where the property names are the same.  For each
191      * property, a conversion is attempted as necessary.  All combinations of
192      * standard JavaBeans and DynaBeans as origin and destination are
193      * supported.  Properties that exist in the origin bean, but do not exist
194      * in the destination bean (or are read-only in the destination bean) are
195      * silently ignored.</p>
196      *
197      * <p>If the origin "bean" is actually a <code>Map</code>, it is assumed
198      * to contain String-valued <strong>simple</strong> property names as the keys, pointing at
199      * the corresponding property values that will be converted (if necessary)
200      * and set in the destination bean. <strong>Note</strong> that this method
201      * is intended to perform a "shallow copy" of the properties and so complex
202      * properties (for example, nested ones) will not be copied.</p>
203      *
204      * <p>This method differs from <code>populate()</code>, which
205      * was primarily designed for populating JavaBeans from the map of request
206      * parameters retrieved on an HTTP request, is that no scalar->indexed
207      * or indexed->scalar manipulations are performed.  If the origin property
208      * is indexed, the destination property must be also.</p>
209      *
210      * <p>If you know that no type conversions are required, the
211      * <code>copyProperties()</code> method in {@link PropertyUtils} will
212      * execute faster than this method.</p>
213      *
214      * <p><strong>FIXME</strong> - Indexed and mapped properties that do not
215      * have getter and setter methods for the underlying array or Map are not
216      * copied by this method.</p>
217      *
218      * @param dest Destination bean whose properties are modified
219      * @param orig Origin bean whose properties are retrieved
220      *
221      * @throws IllegalAccessException if the caller does not have
222      *  access to the property accessor method
223      * @throws IllegalArgumentException if the <code>dest</code> or
224      *  <code>orig</code> argument is null or if the <code>dest</code>
225      *  property type is different from the source type and the relevant
226      *  converter has not been registered.
227      * @throws InvocationTargetException if the property accessor method
228      *  throws an exception
229      */
230     public void copyProperties(final Object dest, final Object orig)
231         throws IllegalAccessException, InvocationTargetException {
232 
233         // Validate existence of the specified beans
234         if (dest == null) {
235             throw new IllegalArgumentException
236                     ("No destination bean specified");
237         }
238         if (orig == null) {
239             throw new IllegalArgumentException("No origin bean specified");
240         }
241         if (log.isDebugEnabled()) {
242             log.debug("BeanUtils.copyProperties(" + dest + ", " +
243                       orig + ")");
244         }
245 
246         // Copy the properties, converting as necessary
247         if (orig instanceof DynaBean) {
248             final DynaProperty[] origDescriptors =
249                 ((DynaBean) orig).getDynaClass().getDynaProperties();
250             for (DynaProperty origDescriptor : origDescriptors) {
251                 final String name = origDescriptor.getName();
252                 // Need to check isReadable() for WrapDynaBean
253                 // (see Jira issue# BEANUTILS-61)
254                 if (getPropertyUtils().isReadable(orig, name) &&
255                     getPropertyUtils().isWriteable(dest, name)) {
256                     final Object value = ((DynaBean) orig).get(name);
257                     copyProperty(dest, name, value);
258                 }
259             }
260         } else if (orig instanceof Map) {
261             @SuppressWarnings("unchecked")
262             final
263             // Map properties are always of type <String, Object>
264             Map<String, Object> propMap = (Map<String, Object>) orig;
265             for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
266                 final String name = entry.getKey();
267                 if (getPropertyUtils().isWriteable(dest, name)) {
268                     copyProperty(dest, name, entry.getValue());
269                 }
270             }
271         } else /* if (orig is a standard JavaBean) */ {
272             final PropertyDescriptor[] origDescriptors =
273                 getPropertyUtils().getPropertyDescriptors(orig);
274             for (PropertyDescriptor origDescriptor : origDescriptors) {
275                 final String name = origDescriptor.getName();
276                 if ("class".equals(name)) {
277                     continue; // No point in trying to set an object's class
278                 }
279                 if (getPropertyUtils().isReadable(orig, name) &&
280                     getPropertyUtils().isWriteable(dest, name)) {
281                     try {
282                         final Object value =
283                             getPropertyUtils().getSimpleProperty(orig, name);
284                         copyProperty(dest, name, value);
285                     } catch (final NoSuchMethodException e) {
286                         // Should not happen
287                     }
288                 }
289             }
290         }
291 
292     }
293 
294 
295     /**
296      * <p>Copy the specified property value to the specified destination bean,
297      * performing any type conversion that is required.  If the specified
298      * bean does not have a property of the specified name, or the property
299      * is read only on the destination bean, return without
300      * doing anything.  If you have custom destination property types, register
301      * {@link Converter}s for them by calling the <code>register()</code>
302      * method of {@link ConvertUtils}.</p>
303      *
304      * <p><strong>IMPLEMENTATION RESTRICTIONS</strong>:</p>
305      * <ul>
306      * <li>Does not support destination properties that are indexed,
307      *     but only an indexed setter (as opposed to an array setter)
308      *     is available.</li>
309      * <li>Does not support destination properties that are mapped,
310      *     but only a keyed setter (as opposed to a Map setter)
311      *     is available.</li>
312      * <li>The desired property type of a mapped setter cannot be
313      *     determined (since Maps support any data type), so no conversion
314      *     will be performed.</li>
315      * </ul>
316      *
317      * @param bean Bean on which setting is to be performed
318      * @param name Property name (can be nested/indexed/mapped/combo)
319      * @param value Value to be set
320      *
321      * @throws IllegalAccessException if the caller does not have
322      *  access to the property accessor method
323      * @throws InvocationTargetException if the property accessor method
324      *  throws an exception
325      */
326     public void copyProperty(final Object bean, String name, Object value)
327         throws IllegalAccessException, InvocationTargetException {
328 
329         // Trace logging (if enabled)
330         if (log.isTraceEnabled()) {
331             final StringBuilder sb = new StringBuilder("  copyProperty(");
332             sb.append(bean);
333             sb.append(", ");
334             sb.append(name);
335             sb.append(", ");
336             if (value == null) {
337                 sb.append("<NULL>");
338             } else if (value instanceof String) {
339                 sb.append((String) value);
340             } else if (value instanceof String[]) {
341                 final String[] values = (String[]) value;
342                 sb.append('[');
343                 for (int i = 0; i < values.length; i++) {
344                     if (i > 0) {
345                         sb.append(',');
346                     }
347                     sb.append(values[i]);
348                 }
349                 sb.append(']');
350             } else {
351                 sb.append(value.toString());
352             }
353             sb.append(')');
354             log.trace(sb.toString());
355         }
356 
357         // Resolve any nested expression to get the actual target bean
358         Object target = bean;
359         final Resolver resolver = getPropertyUtils().getResolver();
360         while (resolver.hasNested(name)) {
361             try {
362                 target = getPropertyUtils().getProperty(target, resolver.next(name));
363                 name = resolver.remove(name);
364             } catch (final NoSuchMethodException e) {
365                 return; // Skip this property setter
366             }
367         }
368         if (log.isTraceEnabled()) {
369             log.trace("    Target bean = " + target);
370             log.trace("    Target name = " + name);
371         }
372 
373         // Declare local variables we will require
374         final String propName = resolver.getProperty(name); // Simple name of target property
375         Class<?> type = null;                         // Java type of target property
376         final int index  = resolver.getIndex(name);         // Indexed subscript value (if any)
377         final String key = resolver.getKey(name);           // Mapped key value (if any)
378 
379         // Calculate the target property type
380         if (target instanceof DynaBean) {
381             final DynaClass dynaClass = ((DynaBean) target).getDynaClass();
382             final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
383             if (dynaProperty == null) {
384                 return; // Skip this property setter
385             }
386             type = dynaPropertyType(dynaProperty, value);
387         } else {
388             PropertyDescriptor descriptor = null;
389             try {
390                 descriptor =
391                     getPropertyUtils().getPropertyDescriptor(target, name);
392                 if (descriptor == null) {
393                     return; // Skip this property setter
394                 }
395             } catch (final NoSuchMethodException e) {
396                 return; // Skip this property setter
397             }
398             type = descriptor.getPropertyType();
399             if (type == null) {
400                 // Most likely an indexed setter on a POJB only
401                 if (log.isTraceEnabled()) {
402                     log.trace("    target type for property '" +
403                               propName + "' is null, so skipping ths setter");
404                 }
405                 return;
406             }
407         }
408         if (log.isTraceEnabled()) {
409             log.trace("    target propName=" + propName + ", type=" +
410                       type + ", index=" + index + ", key=" + key);
411         }
412 
413         // Convert the specified value to the required type and store it
414         if (index >= 0) {                    // Destination must be indexed
415             value = convertForCopy(value, type.getComponentType());
416             try {
417                 getPropertyUtils().setIndexedProperty(target, propName,
418                                                  index, value);
419             } catch (final NoSuchMethodException e) {
420                 throw new InvocationTargetException
421                     (e, "Cannot set " + propName);
422             }
423         } else if (key != null) {            // Destination must be mapped
424             // Maps do not know what the preferred data type is,
425             // so perform no conversions at all
426             // FIXME - should we create or support a TypedMap?
427             try {
428                 getPropertyUtils().setMappedProperty(target, propName,
429                                                 key, value);
430             } catch (final NoSuchMethodException e) {
431                 throw new InvocationTargetException
432                     (e, "Cannot set " + propName);
433             }
434         } else {                             // Destination must be simple
435             value = convertForCopy(value, type);
436             try {
437                 getPropertyUtils().setSimpleProperty(target, propName, value);
438             } catch (final NoSuchMethodException e) {
439                 throw new InvocationTargetException
440                     (e, "Cannot set " + propName);
441             }
442         }
443 
444     }
445 
446 
447     /**
448      * <p>Return the entire set of properties for which the specified bean
449      * provides a read method. This map contains the to <code>String</code>
450      * converted property values for all properties for which a read method
451      * is provided (i.e. where the getReadMethod() returns non-null).</p>
452      *
453      * <p>This map can be fed back to a call to
454      * <code>BeanUtils.populate()</code> to reconsitute the same set of
455      * properties, modulo differences for read-only and write-only
456      * properties, but only if there are no indexed properties.</p>
457      *
458      * <p><strong>Warning:</strong> if any of the bean property implementations
459      * contain (directly or indirectly) a call to this method then
460      * a stack overflow may result. For example:
461      * <code><pre>
462      * class MyBean
463      * {
464      *    public Map getParameterMap()
465      *    {
466      *         BeanUtils.describe(this);
467      *    }
468      * }
469      * </pre></code>
470      * will result in an infinite regression when <code>getParametersMap</code>
471      * is called. It is recommended that such methods are given alternative
472      * names (for example, <code>parametersMap</code>).
473      * </p>
474      * @param bean Bean whose properties are to be extracted
475      * @return Map of property descriptors
476      *
477      * @throws IllegalAccessException if the caller does not have
478      *  access to the property accessor method
479      * @throws InvocationTargetException if the property accessor method
480      *  throws an exception
481      * @throws NoSuchMethodException if an accessor method for this
482      *  property cannot be found
483      */
484     public Map<String, String> describe(final Object bean)
485             throws IllegalAccessException, InvocationTargetException,
486             NoSuchMethodException {
487 
488         if (bean == null) {
489         //            return (Collections.EMPTY_MAP);
490             return (new java.util.HashMap<String, String>());
491         }
492 
493         if (log.isDebugEnabled()) {
494             log.debug("Describing bean: " + bean.getClass().getName());
495         }
496 
497         final Map<String, String> description = new HashMap<String, String>();
498         if (bean instanceof DynaBean) {
499             final DynaProperty[] descriptors =
500                 ((DynaBean) bean).getDynaClass().getDynaProperties();
501             for (DynaProperty descriptor : descriptors) {
502                 final String name = descriptor.getName();
503                 description.put(name, getProperty(bean, name));
504             }
505         } else {
506             final PropertyDescriptor[] descriptors =
507                 getPropertyUtils().getPropertyDescriptors(bean);
508             final Class<?> clazz = bean.getClass();
509             for (PropertyDescriptor descriptor : descriptors) {
510                 final String name = descriptor.getName();
511                 if (getPropertyUtils().getReadMethod(clazz, descriptor) != null) {
512                     description.put(name, getProperty(bean, name));
513                 }
514             }
515         }
516         return (description);
517 
518     }
519 
520 
521     /**
522      * Return the value of the specified array property of the specified
523      * bean, as a String array.
524      *
525      * @param bean Bean whose property is to be extracted
526      * @param name Name of the property to be extracted
527      * @return The array property value
528      *
529      * @throws IllegalAccessException if the caller does not have
530      *  access to the property accessor method
531      * @throws InvocationTargetException if the property accessor method
532      *  throws an exception
533      * @throws NoSuchMethodException if an accessor method for this
534      *  property cannot be found
535      */
536     public String[] getArrayProperty(final Object bean, final String name)
537             throws IllegalAccessException, InvocationTargetException,
538             NoSuchMethodException {
539 
540         final Object value = getPropertyUtils().getProperty(bean, name);
541         if (value == null) {
542             return (null);
543         } else if (value instanceof Collection) {
544             final ArrayList<String> values = new ArrayList<String>();
545             for (final Object item : (Collection<?>) value) {
546                 if (item == null) {
547                     values.add(null);
548                 } else {
549                     // convert to string using convert utils
550                     values.add(getConvertUtils().convert(item));
551                 }
552             }
553             return (values.toArray(new String[values.size()]));
554         } else if (value.getClass().isArray()) {
555             final int n = Array.getLength(value);
556             final String[] results = new String[n];
557             for (int i = 0; i < n; i++) {
558                 final Object item = Array.get(value, i);
559                 if (item == null) {
560                     results[i] = null;
561                 } else {
562                     // convert to string using convert utils
563                     results[i] = getConvertUtils().convert(item);
564                 }
565             }
566             return (results);
567         } else {
568             final String[] results = new String[1];
569             results[0] = getConvertUtils().convert(value);
570             return (results);
571         }
572 
573     }
574 
575 
576     /**
577      * Return the value of the specified indexed property of the specified
578      * bean, as a String.  The zero-relative index of the
579      * required value must be included (in square brackets) as a suffix to
580      * the property name, or <code>IllegalArgumentException</code> will be
581      * thrown.
582      *
583      * @param bean Bean whose property is to be extracted
584      * @param name <code>propertyname[index]</code> of the property value
585      *  to be extracted
586      * @return The indexed property's value, converted to a String
587      *
588      * @throws IllegalAccessException if the caller does not have
589      *  access to the property accessor method
590      * @throws InvocationTargetException if the property accessor method
591      *  throws an exception
592      * @throws NoSuchMethodException if an accessor method for this
593      *  property cannot be found
594      */
595     public String getIndexedProperty(final Object bean, final String name)
596             throws IllegalAccessException, InvocationTargetException,
597             NoSuchMethodException {
598 
599         final Object value = getPropertyUtils().getIndexedProperty(bean, name);
600         return (getConvertUtils().convert(value));
601 
602     }
603 
604 
605     /**
606      * Return the value of the specified indexed property of the specified
607      * bean, as a String.  The index is specified as a method parameter and
608      * must *not* be included in the property name expression
609      *
610      * @param bean Bean whose property is to be extracted
611      * @param name Simple property name of the property value to be extracted
612      * @param index Index of the property value to be extracted
613      * @return The indexed property's value, converted to a String
614      *
615      * @throws IllegalAccessException if the caller does not have
616      *  access to the property accessor method
617      * @throws InvocationTargetException if the property accessor method
618      *  throws an exception
619      * @throws NoSuchMethodException if an accessor method for this
620      *  property cannot be found
621      */
622     public String getIndexedProperty(final Object bean,
623                                             final String name, final int index)
624             throws IllegalAccessException, InvocationTargetException,
625             NoSuchMethodException {
626 
627         final Object value = getPropertyUtils().getIndexedProperty(bean, name, index);
628         return (getConvertUtils().convert(value));
629 
630     }
631 
632 
633     /**
634      * Return the value of the specified indexed property of the specified
635      * bean, as a String.  The String-valued key of the required value
636      * must be included (in parentheses) as a suffix to
637      * the property name, or <code>IllegalArgumentException</code> will be
638      * thrown.
639      *
640      * @param bean Bean whose property is to be extracted
641      * @param name <code>propertyname(index)</code> of the property value
642      *  to be extracted
643      * @return The mapped property's value, converted to a String
644      *
645      * @throws IllegalAccessException if the caller does not have
646      *  access to the property accessor method
647      * @throws InvocationTargetException if the property accessor method
648      *  throws an exception
649      * @throws NoSuchMethodException if an accessor method for this
650      *  property cannot be found
651      */
652     public String getMappedProperty(final Object bean, final String name)
653             throws IllegalAccessException, InvocationTargetException,
654             NoSuchMethodException {
655 
656         final Object value = getPropertyUtils().getMappedProperty(bean, name);
657         return (getConvertUtils().convert(value));
658 
659     }
660 
661 
662     /**
663      * Return the value of the specified mapped property of the specified
664      * bean, as a String.  The key is specified as a method parameter and
665      * must *not* be included in the property name expression
666      *
667      * @param bean Bean whose property is to be extracted
668      * @param name Simple property name of the property value to be extracted
669      * @param key Lookup key of the property value to be extracted
670      * @return The mapped property's value, converted to a String
671      *
672      * @throws IllegalAccessException if the caller does not have
673      *  access to the property accessor method
674      * @throws InvocationTargetException if the property accessor method
675      *  throws an exception
676      * @throws NoSuchMethodException if an accessor method for this
677      *  property cannot be found
678      */
679     public String getMappedProperty(final Object bean,
680                                            final String name, final String key)
681             throws IllegalAccessException, InvocationTargetException,
682             NoSuchMethodException {
683 
684         final Object value = getPropertyUtils().getMappedProperty(bean, name, key);
685         return (getConvertUtils().convert(value));
686 
687     }
688 
689 
690     /**
691      * Return the value of the (possibly nested) property of the specified
692      * name, for the specified bean, as a String.
693      *
694      * @param bean Bean whose property is to be extracted
695      * @param name Possibly nested name of the property to be extracted
696      * @return The nested property's value, converted to a String
697      *
698      * @throws IllegalAccessException if the caller does not have
699      *  access to the property accessor method
700      * @throws IllegalArgumentException if a nested reference to a
701      *  property returns null
702      * @throws InvocationTargetException if the property accessor method
703      *  throws an exception
704      * @throws NoSuchMethodException if an accessor method for this
705      *  property cannot be found
706      */
707     public String getNestedProperty(final Object bean, final String name)
708             throws IllegalAccessException, InvocationTargetException,
709             NoSuchMethodException {
710 
711         final Object value = getPropertyUtils().getNestedProperty(bean, name);
712         return (getConvertUtils().convert(value));
713 
714     }
715 
716 
717     /**
718      * Return the value of the specified property of the specified bean,
719      * no matter which property reference format is used, as a String.
720      *
721      * @param bean Bean whose property is to be extracted
722      * @param name Possibly indexed and/or nested name of the property
723      *  to be extracted
724      * @return The property's value, converted to a String
725      *
726      * @throws IllegalAccessException if the caller does not have
727      *  access to the property accessor method
728      * @throws InvocationTargetException if the property accessor method
729      *  throws an exception
730      * @throws NoSuchMethodException if an accessor method for this
731      *  property cannot be found
732      */
733     public String getProperty(final Object bean, final String name)
734             throws IllegalAccessException, InvocationTargetException,
735             NoSuchMethodException {
736 
737         return (getNestedProperty(bean, name));
738 
739     }
740 
741 
742     /**
743      * Return the value of the specified simple property of the specified
744      * bean, converted to a String.
745      *
746      * @param bean Bean whose property is to be extracted
747      * @param name Name of the property to be extracted
748      * @return The property's value, converted to a String
749      *
750      * @throws IllegalAccessException if the caller does not have
751      *  access to the property accessor method
752      * @throws InvocationTargetException if the property accessor method
753      *  throws an exception
754      * @throws NoSuchMethodException if an accessor method for this
755      *  property cannot be found
756      */
757     public String getSimpleProperty(final Object bean, final String name)
758             throws IllegalAccessException, InvocationTargetException,
759             NoSuchMethodException {
760 
761         final Object value = getPropertyUtils().getSimpleProperty(bean, name);
762         return (getConvertUtils().convert(value));
763 
764     }
765 
766 
767     /**
768      * <p>Populate the JavaBeans properties of the specified bean, based on
769      * the specified name/value pairs.  This method uses Java reflection APIs
770      * to identify corresponding "property setter" method names, and deals
771      * with setter arguments of type <code>String</code>, <code>boolean</code>,
772      * <code>int</code>, <code>long</code>, <code>float</code>, and
773      * <code>double</code>.  In addition, array setters for these types (or the
774      * corresponding primitive types) can also be identified.</p>
775      *
776      * <p>The particular setter method to be called for each property is
777      * determined using the usual JavaBeans introspection mechanisms.  Thus,
778      * you may identify custom setter methods using a BeanInfo class that is
779      * associated with the class of the bean itself.  If no such BeanInfo
780      * class is available, the standard method name conversion ("set" plus
781      * the capitalized name of the property in question) is used.</p>
782      *
783      * <p><strong>NOTE</strong>:  It is contrary to the JavaBeans Specification
784      * to have more than one setter method (with different argument
785      * signatures) for the same property.</p>
786      *
787      * <p><strong>WARNING</strong> - The logic of this method is customized
788      * for extracting String-based request parameters from an HTTP request.
789      * It is probably not what you want for general property copying with
790      * type conversion.  For that purpose, check out the
791      * <code>copyProperties()</code> method instead.</p>
792      *
793      * @param bean JavaBean whose properties are being populated
794      * @param properties Map keyed by property name, with the
795      *  corresponding (String or String[]) value(s) to be set
796      *
797      * @throws IllegalAccessException if the caller does not have
798      *  access to the property accessor method
799      * @throws InvocationTargetException if the property accessor method
800      *  throws an exception
801      */
802     public void populate(final Object bean, final Map<String, ? extends Object> properties)
803         throws IllegalAccessException, InvocationTargetException {
804 
805         // Do nothing unless both arguments have been specified
806         if ((bean == null) || (properties == null)) {
807             return;
808         }
809         if (log.isDebugEnabled()) {
810             log.debug("BeanUtils.populate(" + bean + ", " +
811                     properties + ")");
812         }
813 
814         // Loop through the property name/value pairs to be set
815         for(final Map.Entry<String, ? extends Object> entry : properties.entrySet()) {
816             // Identify the property name and value(s) to be assigned
817             final String name = entry.getKey();
818             if (name == null) {
819                 continue;
820             }
821 
822             // Perform the assignment for this property
823             setProperty(bean, name, entry.getValue());
824 
825         }
826 
827     }
828 
829 
830     /**
831      * <p>Set the specified property value, performing type conversions as
832      * required to conform to the type of the destination property.</p>
833      *
834      * <p>If the property is read only then the method returns
835      * without throwing an exception.</p>
836      *
837      * <p>If <code>null</code> is passed into a property expecting a primitive value,
838      * then this will be converted as if it were a <code>null</code> string.</p>
839      *
840      * <p><strong>WARNING</strong> - The logic of this method is customized
841      * to meet the needs of <code>populate()</code>, and is probably not what
842      * you want for general property copying with type conversion.  For that
843      * purpose, check out the <code>copyProperty()</code> method instead.</p>
844      *
845      * <p><strong>WARNING</strong> - PLEASE do not modify the behavior of this
846      * method without consulting with the Struts developer community.  There
847      * are some subtleties to its functionality that are not documented in the
848      * Javadoc description above, yet are vital to the way that Struts utilizes
849      * this method.</p>
850      *
851      * @param bean Bean on which setting is to be performed
852      * @param name Property name (can be nested/indexed/mapped/combo)
853      * @param value Value to be set
854      *
855      * @throws IllegalAccessException if the caller does not have
856      *  access to the property accessor method
857      * @throws InvocationTargetException if the property accessor method
858      *  throws an exception
859      */
860     public void setProperty(final Object bean, String name, final Object value)
861         throws IllegalAccessException, InvocationTargetException {
862 
863         // Trace logging (if enabled)
864         if (log.isTraceEnabled()) {
865             final StringBuilder sb = new StringBuilder("  setProperty(");
866             sb.append(bean);
867             sb.append(", ");
868             sb.append(name);
869             sb.append(", ");
870             if (value == null) {
871                 sb.append("<NULL>");
872             } else if (value instanceof String) {
873                 sb.append((String) value);
874             } else if (value instanceof String[]) {
875                 final String[] values = (String[]) value;
876                 sb.append('[');
877                 for (int i = 0; i < values.length; i++) {
878                     if (i > 0) {
879                         sb.append(',');
880                     }
881                     sb.append(values[i]);
882                 }
883                 sb.append(']');
884             } else {
885                 sb.append(value.toString());
886             }
887             sb.append(')');
888             log.trace(sb.toString());
889         }
890 
891         // Resolve any nested expression to get the actual target bean
892         Object target = bean;
893         final Resolver resolver = getPropertyUtils().getResolver();
894         while (resolver.hasNested(name)) {
895             try {
896                 target = getPropertyUtils().getProperty(target, resolver.next(name));
897                 if (target == null) { // the value of a nested property is null
898                     return;
899                 }
900                 name = resolver.remove(name);
901             } catch (final NoSuchMethodException e) {
902                 return; // Skip this property setter
903             }
904         }
905         if (log.isTraceEnabled()) {
906             log.trace("    Target bean = " + target);
907             log.trace("    Target name = " + name);
908         }
909 
910         // Declare local variables we will require
911         final String propName = resolver.getProperty(name); // Simple name of target property
912         Class<?> type = null;                         // Java type of target property
913         final int index  = resolver.getIndex(name);         // Indexed subscript value (if any)
914         final String key = resolver.getKey(name);           // Mapped key value (if any)
915 
916         // Calculate the property type
917         if (target instanceof DynaBean) {
918             final DynaClass dynaClass = ((DynaBean) target).getDynaClass();
919             final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
920             if (dynaProperty == null) {
921                 return; // Skip this property setter
922             }
923             type = dynaPropertyType(dynaProperty, value);
924             if (index >= 0 && List.class.isAssignableFrom(type)) {
925             	type = Object.class;
926             }
927         } else if (target instanceof Map) {
928             type = Object.class;
929         } else if (target != null && target.getClass().isArray() && index >= 0) {
930             type = Array.get(target, index).getClass();
931         } else {
932             PropertyDescriptor descriptor = null;
933             try {
934                 descriptor =
935                     getPropertyUtils().getPropertyDescriptor(target, name);
936                 if (descriptor == null) {
937                     return; // Skip this property setter
938                 }
939             } catch (final NoSuchMethodException e) {
940                 return; // Skip this property setter
941             }
942             if (descriptor instanceof MappedPropertyDescriptor) {
943                 if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) {
944                     if (log.isDebugEnabled()) {
945                         log.debug("Skipping read-only property");
946                     }
947                     return; // Read-only, skip this property setter
948                 }
949                 type = ((MappedPropertyDescriptor) descriptor).
950                     getMappedPropertyType();
951             } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) {
952                 if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) {
953                     if (log.isDebugEnabled()) {
954                         log.debug("Skipping read-only property");
955                     }
956                     return; // Read-only, skip this property setter
957                 }
958                 type = ((IndexedPropertyDescriptor) descriptor).
959                     getIndexedPropertyType();
960             } else if (index >= 0 && List.class.isAssignableFrom(descriptor.getPropertyType())) {
961                 type = Object.class;
962             } else if (key != null) {
963                 if (descriptor.getReadMethod() == null) {
964                     if (log.isDebugEnabled()) {
965                         log.debug("Skipping read-only property");
966                     }
967                     return; // Read-only, skip this property setter
968                 }
969                 type = (value == null) ? Object.class : value.getClass();
970             } else {
971                 if (descriptor.getWriteMethod() == null) {
972                     if (log.isDebugEnabled()) {
973                         log.debug("Skipping read-only property");
974                     }
975                     return; // Read-only, skip this property setter
976                 }
977                 type = descriptor.getPropertyType();
978             }
979         }
980 
981         // Convert the specified value to the required type
982         Object newValue = null;
983         if (type.isArray() && (index < 0)) { // Scalar value into array
984             if (value == null) {
985                 final String[] values = new String[1];
986                 values[0] = null;
987                 newValue = getConvertUtils().convert(values, type);
988             } else if (value instanceof String) {
989                 newValue = getConvertUtils().convert(value, type);
990             } else if (value instanceof String[]) {
991                 newValue = getConvertUtils().convert((String[]) value, type);
992             } else {
993                 newValue = convert(value, type);
994             }
995         } else if (type.isArray()) {         // Indexed value into array
996             if (value instanceof String || value == null) {
997                 newValue = getConvertUtils().convert((String) value,
998                                                 type.getComponentType());
999             } else if (value instanceof String[]) {
1000                 newValue = getConvertUtils().convert(((String[]) value)[0],
1001                                                 type.getComponentType());
1002             } else {
1003                 newValue = convert(value, type.getComponentType());
1004             }
1005         } else {                             // Value into scalar
1006             if (value instanceof String) {
1007                 newValue = getConvertUtils().convert((String) value, type);
1008             } else if (value instanceof String[]) {
1009                 newValue = getConvertUtils().convert(((String[]) value)[0],
1010                                                 type);
1011             } else {
1012                 newValue = convert(value, type);
1013             }
1014         }
1015 
1016         // Invoke the setter method
1017         try {
1018           getPropertyUtils().setProperty(target, name, newValue);
1019         } catch (final NoSuchMethodException e) {
1020             throw new InvocationTargetException
1021                 (e, "Cannot set " + propName);
1022         }
1023 
1024     }
1025 
1026     /**
1027      * Gets the <code>ConvertUtilsBean</code> instance used to perform the conversions.
1028      *
1029      * @return The ConvertUtils bean instance
1030      */
1031     public ConvertUtilsBean getConvertUtils() {
1032         return convertUtilsBean;
1033     }
1034 
1035     /**
1036      * Gets the <code>PropertyUtilsBean</code> instance used to access properties.
1037      *
1038      * @return The ConvertUtils bean instance
1039      */
1040     public PropertyUtilsBean getPropertyUtils() {
1041         return propertyUtilsBean;
1042     }
1043 
1044     /**
1045      * If we're running on JDK 1.4 or later, initialize the cause for the given throwable.
1046      *
1047      * @param  throwable The throwable.
1048      * @param  cause     The cause of the throwable.
1049      * @return  true if the cause was initialized, otherwise false.
1050      * @since 1.8.0
1051      */
1052     public boolean initCause(final Throwable throwable, final Throwable cause) {
1053         if (INIT_CAUSE_METHOD != null && cause != null) {
1054             try {
1055                 INIT_CAUSE_METHOD.invoke(throwable, new Object[] { cause });
1056                 return true;
1057             } catch (final Throwable e) {
1058                 return false; // can't initialize cause
1059             }
1060         }
1061         return false;
1062     }
1063 
1064     /**
1065      * <p>Convert the value to an object of the specified class (if
1066      * possible).</p>
1067      *
1068      * @param value Value to be converted (may be null)
1069      * @param type Class of the value to be converted to
1070      * @return The converted value
1071      *
1072      * @throws ConversionException if thrown by an underlying Converter
1073      * @since 1.8.0
1074      */
1075     protected Object convert(final Object value, final Class<?> type) {
1076         final Converter converter = getConvertUtils().lookup(type);
1077         if (converter != null) {
1078             log.trace("        USING CONVERTER " + converter);
1079             return converter.convert(type, value);
1080         } else {
1081             return value;
1082         }
1083     }
1084 
1085     /**
1086      * Performs a type conversion of a property value before it is copied to a target
1087      * bean. This method delegates to {@link #convert(Object, Class)}, but <b>null</b>
1088      * values are not converted. This causes <b>null</b> values to be copied verbatim.
1089      *
1090      * @param value the value to be converted and copied
1091      * @param type the target type of the conversion
1092      * @return the converted value
1093      */
1094     private Object convertForCopy(final Object value, final Class<?> type) {
1095         return (value != null) ? convert(value, type) : value;
1096     }
1097 
1098     /**
1099      * Returns a <code>Method<code> allowing access to
1100      * {@link Throwable#initCause(Throwable)} method of {@link Throwable},
1101      * or <code>null</code> if the method
1102      * does not exist.
1103      *
1104      * @return A <code>Method<code> for <code>Throwable.initCause</code>, or
1105      * <code>null</code> if unavailable.
1106      */
1107     private static Method getInitCauseMethod() {
1108         try {
1109             final Class<?>[] paramsClasses = new Class<?>[] { Throwable.class };
1110             return Throwable.class.getMethod("initCause", paramsClasses);
1111         } catch (final NoSuchMethodException e) {
1112             final Log log = LogFactory.getLog(BeanUtils.class);
1113             if (log.isWarnEnabled()) {
1114                 log.warn("Throwable does not have initCause() method in JDK 1.3");
1115             }
1116             return null;
1117         } catch (final Throwable e) {
1118             final Log log = LogFactory.getLog(BeanUtils.class);
1119             if (log.isWarnEnabled()) {
1120                 log.warn("Error getting the Throwable initCause() method", e);
1121             }
1122             return null;
1123         }
1124     }
1125 
1126     /**
1127      * Determines the type of a {@code DynaProperty}. Here a special treatment
1128      * is needed for mapped properties.
1129      *
1130      * @param dynaProperty the property descriptor
1131      * @param value the value object to be set for this property
1132      * @return the type of this property
1133      */
1134     private static Class<?> dynaPropertyType(final DynaProperty dynaProperty,
1135             final Object value) {
1136         if (!dynaProperty.isMapped()) {
1137             return dynaProperty.getType();
1138         }
1139         return (value == null) ? String.class : value.getClass();
1140     }
1141 }