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 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.beanutils;
019
020import java.beans.IndexedPropertyDescriptor;
021import java.beans.PropertyDescriptor;
022import java.lang.reflect.Array;
023import java.lang.reflect.InvocationTargetException;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import org.apache.commons.beanutils.expression.Resolver;
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033
034/**
035 * <p>JavaBean property population methods.</p>
036 *
037 * <p>This class provides implementations for the utility methods in
038 * {@link BeanUtils}.
039 * Different instances can be used to isolate caches between classloaders
040 * and to vary the value converters registered.</p>
041 *
042 * @see BeanUtils
043 * @since 1.7
044 */
045public class BeanUtilsBean {
046
047    /**
048     * Contains <code>BeanUtilsBean</code> instances indexed by context classloader.
049     */
050    private static final ContextClassLoaderLocal<BeanUtilsBean>
051            BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
052                        // Creates the default instance used when the context classloader is unavailable
053                        @Override
054                        protected BeanUtilsBean initialValue() {
055                            return new BeanUtilsBean();
056                        }
057                    };
058
059    /**
060     * Determines the type of a {@code DynaProperty}. Here a special treatment
061     * is needed for mapped properties.
062     *
063     * @param dynaProperty the property descriptor
064     * @param value the value object to be set for this property
065     * @return the type of this property
066     */
067    private static Class<?> dynaPropertyType(final DynaProperty dynaProperty,
068            final Object value) {
069        if (!dynaProperty.isMapped()) {
070            return dynaProperty.getType();
071        }
072        return value == null ? String.class : value.getClass();
073    }
074
075    /**
076     * Gets the instance which provides the functionality for {@link BeanUtils}.
077     * This is a pseudo-singleton - an single instance is provided per (thread) context classloader.
078     * This mechanism provides isolation for web apps deployed in the same container.
079     *
080     * @return The (pseudo-singleton) BeanUtils bean instance
081     */
082    public static BeanUtilsBean getInstance() {
083        return BEANS_BY_CLASSLOADER.get();
084    }
085
086    /**
087     * Sets the instance which provides the functionality for {@link BeanUtils}.
088     * This is a pseudo-singleton - an single instance is provided per (thread) context classloader.
089     * This mechanism provides isolation for web apps deployed in the same container.
090     *
091     * @param newInstance The (pseudo-singleton) BeanUtils bean instance
092     */
093    public static void setInstance(final BeanUtilsBean newInstance) {
094        BEANS_BY_CLASSLOADER.set(newInstance);
095    }
096
097    /**
098     * Logging for this instance
099     */
100    private final Log log = LogFactory.getLog(BeanUtils.class);
101
102    /** Used to perform conversions between object types when setting properties */
103    private final ConvertUtilsBean convertUtilsBean;
104
105    /** Used to access properties*/
106    private final PropertyUtilsBean propertyUtilsBean;
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(final ConvertUtilsBean convertUtilsBean, final PropertyUtilsBean propertyUtilsBean) {
138        this.convertUtilsBean = convertUtilsBean;
139        this.propertyUtilsBean = propertyUtilsBean;
140    }
141
142    /**
143     * <p>Clone a bean based on the available property getters and setters,
144     * even if the bean class itself does not implement Cloneable.</p>
145     *
146     * <p>
147     * <strong>Note:</strong> this method creates a <strong>shallow</strong> clone.
148     * In other words, any objects referred to by the bean are shared with the clone
149     * rather than being cloned in turn.
150     * </p>
151     *
152     * @param bean Bean to be cloned
153     * @return the cloned bean
154     * @throws IllegalAccessException if the caller does not have
155     *  access to the property accessor method
156     * @throws InstantiationException if a new instance of the bean's
157     *  class cannot be instantiated
158     * @throws InvocationTargetException if the property accessor method
159     *  throws an exception
160     * @throws NoSuchMethodException if an accessor method for this
161     *  property cannot be found
162     */
163    public Object cloneBean(final Object bean) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
164        if (log.isDebugEnabled()) {
165            log.debug("Cloning bean: " + bean.getClass().getName());
166        }
167        Object newBean = null;
168        if (bean instanceof DynaBean) {
169            newBean = ((DynaBean) bean).getDynaClass().newInstance();
170        } else {
171            newBean = bean.getClass().getConstructor().newInstance();
172        }
173        getPropertyUtils().copyProperties(newBean, bean);
174        return newBean;
175    }
176
177    /**
178     * <p>Convert the value to an object of the specified class (if
179     * possible).</p>
180     *
181     * @param value Value to be converted (may be null)
182     * @param type Class of the value to be converted to
183     * @return The converted value
184     * @throws ConversionException if thrown by an underlying Converter
185     * @since 1.8.0
186     */
187    protected Object convert(final Object value, final Class<?> type) {
188        final Converter converter = getConvertUtils().lookup(type);
189        if (converter != null) {
190            log.trace("        USING CONVERTER " + converter);
191            return converter.convert(type, value);
192        }
193        return value;
194    }
195
196    /**
197     * Performs a type conversion of a property value before it is copied to a target
198     * bean. This method delegates to {@link #convert(Object, Class)}, but <strong>null</strong>
199     * values are not converted. This causes <strong>null</strong> values to be copied verbatim.
200     *
201     * @param value the value to be converted and copied
202     * @param type the target type of the conversion
203     * @return the converted value
204     */
205    private Object convertForCopy(final Object value, final Class<?> type) {
206        return value != null ? convert(value, type) : value;
207    }
208
209    /**
210     * <p>Copy property values from the origin bean to the destination bean
211     * for all cases where the property names are the same.  For each
212     * property, a conversion is attempted as necessary.  All combinations of
213     * standard JavaBeans and DynaBeans as origin and destination are
214     * supported.  Properties that exist in the origin bean, but do not exist
215     * in the destination bean (or are read-only in the destination bean) are
216     * silently ignored.</p>
217     *
218     * <p>If the origin "bean" is actually a <code>Map</code>, it is assumed
219     * to contain String-valued <strong>simple</strong> property names as the keys, pointing at
220     * the corresponding property values that will be converted (if necessary)
221     * and set in the destination bean. <strong>Note</strong> that this method
222     * is intended to perform a "shallow copy" of the properties and so complex
223     * properties (for example, nested ones) will not be copied.</p>
224     *
225     * <p>This method differs from <code>populate()</code>, which
226     * was primarily designed for populating JavaBeans from the map of request
227     * parameters retrieved on an HTTP request, is that no scalar to indexed
228     * or indexed to scalar manipulations are performed.  If the origin property
229     * is indexed, the destination property must be also.</p>
230     *
231     * <p>If you know that no type conversions are required, the
232     * <code>copyProperties()</code> method in {@link PropertyUtils} will
233     * execute faster than this method.</p>
234     *
235     * <p><strong>FIXME</strong> - Indexed and mapped properties that do not
236     * have getter and setter methods for the underlying array or Map are not
237     * copied by this method.</p>
238     *
239     * @param dest Destination bean whose properties are modified
240     * @param orig Origin bean whose properties are retrieved
241     * @throws IllegalAccessException if the caller does not have
242     *  access to the property accessor method
243     * @throws IllegalArgumentException if the <code>dest</code> or
244     *  <code>orig</code> argument is null or if the <code>dest</code>
245     *  property type is different from the source type and the relevant
246     *  converter has not been registered.
247     * @throws InvocationTargetException if the property accessor method
248     *  throws an exception
249     */
250    public void copyProperties(final Object dest, final Object orig)
251        throws IllegalAccessException, InvocationTargetException {
252
253        // Validate existence of the specified beans
254        if (dest == null) {
255            throw new IllegalArgumentException
256                    ("No destination bean specified");
257        }
258        if (orig == null) {
259            throw new IllegalArgumentException("No origin bean specified");
260        }
261        if (log.isDebugEnabled()) {
262            log.debug("BeanUtils.copyProperties(" + dest + ", " +
263                      orig + ")");
264        }
265
266        // Copy the properties, converting as necessary
267        if (orig instanceof DynaBean) {
268            final DynaProperty[] origDescriptors =
269                ((DynaBean) orig).getDynaClass().getDynaProperties();
270            for (final DynaProperty origDescriptor : origDescriptors) {
271                final String name = origDescriptor.getName();
272                // Need to check isReadable() for WrapDynaBean
273                // (see Jira issue# BEANUTILS-61)
274                if (getPropertyUtils().isReadable(orig, name) &&
275                    getPropertyUtils().isWriteable(dest, name)) {
276                    final Object value = ((DynaBean) orig).get(name);
277                    copyProperty(dest, name, value);
278                }
279            }
280        } else if (orig instanceof Map) {
281            @SuppressWarnings("unchecked")
282            final
283            // Map properties are always of type <String, Object>
284            Map<String, Object> propMap = (Map<String, Object>) orig;
285            for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
286                final String name = entry.getKey();
287                if (getPropertyUtils().isWriteable(dest, name)) {
288                    copyProperty(dest, name, entry.getValue());
289                }
290            }
291        } else /* if (orig is a standard JavaBean) */ {
292            final PropertyDescriptor[] origDescriptors =
293                getPropertyUtils().getPropertyDescriptors(orig);
294            for (final PropertyDescriptor origDescriptor : origDescriptors) {
295                final String name = origDescriptor.getName();
296                if ("class".equals(name)) {
297                    continue; // No point in trying to set an object's class
298                }
299                if (getPropertyUtils().isReadable(orig, name) &&
300                    getPropertyUtils().isWriteable(dest, name)) {
301                    try {
302                        final Object value =
303                            getPropertyUtils().getSimpleProperty(orig, name);
304                        copyProperty(dest, name, value);
305                    } catch (final NoSuchMethodException e) {
306                        // Should not happen
307                    }
308                }
309            }
310        }
311
312    }
313
314    /**
315     * <p>Copy the specified property value to the specified destination bean,
316     * performing any type conversion that is required.  If the specified
317     * bean does not have a property of the specified name, or the property
318     * is read only on the destination bean, return without
319     * doing anything.  If you have custom destination property types, register
320     * {@link Converter}s for them by calling the <code>register()</code>
321     * method of {@link ConvertUtils}.</p>
322     *
323     * <p><strong>IMPLEMENTATION RESTRICTIONS</strong>:</p>
324     * <ul>
325     * <li>Does not support destination properties that are indexed,
326     *     but only an indexed setter (as opposed to an array setter)
327     *     is available.</li>
328     * <li>Does not support destination properties that are mapped,
329     *     but only a keyed setter (as opposed to a Map setter)
330     *     is available.</li>
331     * <li>The desired property type of a mapped setter cannot be
332     *     determined (since Maps support any data type), so no conversion
333     *     will be performed.</li>
334     * </ul>
335     *
336     * @param bean Bean on which setting is to be performed
337     * @param name Property name (can be nested/indexed/mapped/combo)
338     * @param value Value to be set
339     * @throws IllegalAccessException if the caller does not have
340     *  access to the property accessor method
341     * @throws InvocationTargetException if the property accessor method
342     *  throws an exception
343     */
344    public void copyProperty(final Object bean, String name, Object value)
345        throws IllegalAccessException, InvocationTargetException {
346
347        // Trace logging (if enabled)
348        if (log.isTraceEnabled()) {
349            final StringBuilder sb = new StringBuilder("  copyProperty(");
350            sb.append(bean);
351            sb.append(", ");
352            sb.append(name);
353            sb.append(", ");
354            if (value == null) {
355                sb.append("<NULL>");
356            } else if (value instanceof String) {
357                sb.append((String) value);
358            } else if (value instanceof String[]) {
359                final String[] values = (String[]) value;
360                sb.append('[');
361                for (int i = 0; i < values.length; i++) {
362                    if (i > 0) {
363                        sb.append(',');
364                    }
365                    sb.append(values[i]);
366                }
367                sb.append(']');
368            } else {
369                sb.append(value.toString());
370            }
371            sb.append(')');
372            log.trace(sb.toString());
373        }
374
375        // Resolve any nested expression to get the actual target bean
376        Object target = bean;
377        final Resolver resolver = getPropertyUtils().getResolver();
378        while (resolver.hasNested(name)) {
379            try {
380                target = getPropertyUtils().getProperty(target, resolver.next(name));
381                name = resolver.remove(name);
382            } catch (final NoSuchMethodException e) {
383                return; // Skip this property setter
384            }
385        }
386        if (log.isTraceEnabled()) {
387            log.trace("    Target bean = " + target);
388            log.trace("    Target name = " + name);
389        }
390
391        // Declare local variables we will require
392        final String propName = resolver.getProperty(name); // Simple name of target property
393        Class<?> type = null;                         // Java type of target property
394        final int index  = resolver.getIndex(name);         // Indexed subscript value (if any)
395        final String key = resolver.getKey(name);           // Mapped key value (if any)
396
397        // Calculate the target property type
398        if (target instanceof DynaBean) {
399            final DynaClass dynaClass = ((DynaBean) target).getDynaClass();
400            final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
401            if (dynaProperty == null) {
402                return; // Skip this property setter
403            }
404            type = dynaPropertyType(dynaProperty, value);
405        } else {
406            PropertyDescriptor descriptor = null;
407            try {
408                descriptor =
409                    getPropertyUtils().getPropertyDescriptor(target, name);
410                if (descriptor == null) {
411                    return; // Skip this property setter
412                }
413            } catch (final NoSuchMethodException e) {
414                return; // Skip this property setter
415            }
416            type = descriptor.getPropertyType();
417            if (type == null) {
418                // Most likely an indexed setter on a POJB only
419                if (log.isTraceEnabled()) {
420                    log.trace("    target type for property '" + propName + "' is null, so skipping the setter");
421                }
422                return;
423            }
424        }
425        if (log.isTraceEnabled()) {
426            log.trace("    target propName=" + propName + ", type=" +
427                      type + ", index=" + index + ", key=" + key);
428        }
429
430        // Convert the specified value to the required type and store it
431        if (index >= 0) {                    // Destination must be indexed
432            value = convertForCopy(value, type.getComponentType());
433            try {
434                getPropertyUtils().setIndexedProperty(target, propName,
435                                                 index, value);
436            } catch (final NoSuchMethodException e) {
437                throw new InvocationTargetException
438                    (e, "Cannot set " + propName);
439            }
440        } else if (key != null) {            // Destination must be mapped
441            // Maps do not know what the preferred data type is,
442            // so perform no conversions at all
443            // FIXME - should we create or support a TypedMap?
444            try {
445                getPropertyUtils().setMappedProperty(target, propName,
446                                                key, value);
447            } catch (final NoSuchMethodException e) {
448                throw new InvocationTargetException
449                    (e, "Cannot set " + propName);
450            }
451        } else {                             // Destination must be simple
452            value = convertForCopy(value, type);
453            try {
454                getPropertyUtils().setSimpleProperty(target, propName, value);
455            } catch (final NoSuchMethodException e) {
456                throw new InvocationTargetException
457                    (e, "Cannot set " + propName);
458            }
459        }
460
461    }
462
463    /**
464     * <p>Return the entire set of properties for which the specified bean
465     * provides a read method. This map contains the to <code>String</code>
466     * converted property values for all properties for which a read method
467     * is provided (i.e. where the getReadMethod() returns non-null).</p>
468     *
469     * <p>This map can be fed back to a call to
470     * <code>BeanUtils.populate()</code> to reconsitute the same set of
471     * properties, modulo differences for read-only and write-only
472     * properties, but only if there are no indexed properties.</p>
473     *
474     * <p><strong>Warning:</strong> if any of the bean property implementations
475     * contain (directly or indirectly) a call to this method then
476     * a stack overflow may result. For example:
477     * </p>
478     * <pre>
479     * class MyBean
480     * {
481     *    public Map getParameterMap()
482     *    {
483     *         BeanUtils.describe(this);
484     *    }
485     * }
486     * </pre>
487     * <p>
488     * will result in an infinite regression when <code>getParametersMap</code>
489     * is called. It is recommended that such methods are given alternative
490     * names (for example, <code>parametersMap</code>).
491     * </p>
492     * @param bean Bean whose properties are to be extracted
493     * @return Map of property descriptors
494     * @throws IllegalAccessException if the caller does not have
495     *  access to the property accessor method
496     * @throws InvocationTargetException if the property accessor method
497     *  throws an exception
498     * @throws NoSuchMethodException if an accessor method for this
499     *  property cannot be found
500     */
501    public Map<String, String> describe(final Object bean)
502            throws IllegalAccessException, InvocationTargetException,
503            NoSuchMethodException {
504
505        if (bean == null) {
506        //            return (Collections.EMPTY_MAP);
507            return new java.util.HashMap<>();
508        }
509
510        if (log.isDebugEnabled()) {
511            log.debug("Describing bean: " + bean.getClass().getName());
512        }
513
514        final Map<String, String> description = new HashMap<>();
515        if (bean instanceof DynaBean) {
516            final DynaProperty[] descriptors =
517                ((DynaBean) bean).getDynaClass().getDynaProperties();
518            for (final DynaProperty descriptor : descriptors) {
519                final String name = descriptor.getName();
520                description.put(name, getProperty(bean, name));
521            }
522        } else {
523            final PropertyDescriptor[] descriptors =
524                getPropertyUtils().getPropertyDescriptors(bean);
525            final Class<?> clazz = bean.getClass();
526            for (final PropertyDescriptor descriptor : descriptors) {
527                final String name = descriptor.getName();
528                if (getPropertyUtils().getReadMethod(clazz, descriptor) != null) {
529                    description.put(name, getProperty(bean, name));
530                }
531            }
532        }
533        return description;
534
535    }
536
537    /**
538     * Return the value of the specified array property of the specified
539     * bean, as a String array.
540     *
541     * @param bean Bean whose property is to be extracted
542     * @param name Name of the property to be extracted
543     * @return The array property value
544     * @throws IllegalAccessException if the caller does not have
545     *  access to the property accessor method
546     * @throws InvocationTargetException if the property accessor method
547     *  throws an exception
548     * @throws NoSuchMethodException if an accessor method for this
549     *  property cannot be found
550     */
551    public String[] getArrayProperty(final Object bean, final String name)
552            throws IllegalAccessException, InvocationTargetException,
553            NoSuchMethodException {
554
555        final Object value = getPropertyUtils().getProperty(bean, name);
556        if (value == null) {
557            return null;
558        }
559        if (value instanceof Collection) {
560            final ArrayList<String> values = new ArrayList<>();
561            for (final Object item : (Collection<?>) value) {
562                if (item == null) {
563                    values.add(null);
564                } else {
565                    // convert to string using convert utils
566                    values.add(getConvertUtils().convert(item));
567                }
568            }
569            return values.toArray(new String[values.size()]);
570        }
571        if (!value.getClass().isArray()) {
572            final String[] results = new String[1];
573            results[0] = getConvertUtils().convert(value);
574            return results;
575        }
576        final int n = Array.getLength(value);
577        final String[] results = new String[n];
578        for (int i = 0; i < n; i++) {
579            final Object item = Array.get(value, i);
580            if (item == null) {
581                results[i] = null;
582            } else {
583                // convert to string using convert utils
584                results[i] = getConvertUtils().convert(item);
585            }
586        }
587        return results;
588
589    }
590
591    /**
592     * Gets the <code>ConvertUtilsBean</code> instance used to perform the conversions.
593     *
594     * @return The ConvertUtils bean instance
595     */
596    public ConvertUtilsBean getConvertUtils() {
597        return convertUtilsBean;
598    }
599
600    /**
601     * Return the value of the specified indexed property of the specified
602     * bean, as a String.  The zero-relative index of the
603     * required value must be included (in square brackets) as a suffix to
604     * the property name, or <code>IllegalArgumentException</code> will be
605     * thrown.
606     *
607     * @param bean Bean whose property is to be extracted
608     * @param name <code>propertyname[index]</code> of the property value
609     *  to be extracted
610     * @return The indexed property's value, converted to a String
611     * @throws IllegalAccessException if the caller does not have
612     *  access to the property accessor method
613     * @throws InvocationTargetException if the property accessor method
614     *  throws an exception
615     * @throws NoSuchMethodException if an accessor method for this
616     *  property cannot be found
617     */
618    public String getIndexedProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
619        final Object value = getPropertyUtils().getIndexedProperty(bean, name);
620        return getConvertUtils().convert(value);
621    }
622
623    /**
624     * Return the value of the specified indexed property of the specified
625     * bean, as a String.  The index is specified as a method parameter and
626     * must *not* be included in the property name expression
627     *
628     * @param bean Bean whose property is to be extracted
629     * @param name Simple property name of the property value to be extracted
630     * @param index Index of the property value to be extracted
631     * @return The indexed property's value, converted to a String
632     * @throws IllegalAccessException if the caller does not have
633     *  access to the property accessor method
634     * @throws InvocationTargetException if the property accessor method
635     *  throws an exception
636     * @throws NoSuchMethodException if an accessor method for this
637     *  property cannot be found
638     */
639    public String getIndexedProperty(final Object bean, final String name, final int index)
640            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
641        final Object value = getPropertyUtils().getIndexedProperty(bean, name, index);
642        return getConvertUtils().convert(value);
643    }
644
645    /**
646     * Return the value of the specified indexed property of the specified
647     * bean, as a String.  The String-valued key of the required value
648     * must be included (in parentheses) as a suffix to
649     * the property name, or <code>IllegalArgumentException</code> will be
650     * thrown.
651     *
652     * @param bean Bean whose property is to be extracted
653     * @param name <code>propertyname(index)</code> of the property value
654     *  to be extracted
655     * @return The mapped property's value, converted to a String
656     * @throws IllegalAccessException if the caller does not have
657     *  access to the property accessor method
658     * @throws InvocationTargetException if the property accessor method
659     *  throws an exception
660     * @throws NoSuchMethodException if an accessor method for this
661     *  property cannot be found
662     */
663    public String getMappedProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
664        final Object value = getPropertyUtils().getMappedProperty(bean, name);
665        return getConvertUtils().convert(value);
666    }
667
668    /**
669     * Return the value of the specified mapped property of the specified
670     * bean, as a String.  The key is specified as a method parameter and
671     * must *not* be included in the property name expression
672     *
673     * @param bean Bean whose property is to be extracted
674     * @param name Simple property name of the property value to be extracted
675     * @param key Lookup key of the property value to be extracted
676     * @return The mapped property's value, converted to a String
677     * @throws IllegalAccessException if the caller does not have
678     *  access to the property accessor method
679     * @throws InvocationTargetException if the property accessor method
680     *  throws an exception
681     * @throws NoSuchMethodException if an accessor method for this
682     *  property cannot be found
683     */
684    public String getMappedProperty(final Object bean, final String name, final String key)
685            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
686        final Object value = getPropertyUtils().getMappedProperty(bean, name, key);
687        return getConvertUtils().convert(value);
688
689    }
690
691    /**
692     * Return the value of the (possibly nested) property of the specified
693     * name, for the specified bean, as a String.
694     *
695     * @param bean Bean whose property is to be extracted
696     * @param name Possibly nested name of the property to be extracted
697     * @return The nested property's value, converted to a String
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) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
708        final Object value = getPropertyUtils().getNestedProperty(bean, name);
709        return getConvertUtils().convert(value);
710    }
711
712    /**
713     * Return the value of the specified property of the specified bean,
714     * no matter which property reference format is used, as a String.
715     *
716     * @param bean Bean whose property is to be extracted
717     * @param name Possibly indexed and/or nested name of the property
718     *  to be extracted
719     * @return The property's value, converted to a String
720     * @throws IllegalAccessException if the caller does not have
721     *  access to the property accessor method
722     * @throws InvocationTargetException if the property accessor method
723     *  throws an exception
724     * @throws NoSuchMethodException if an accessor method for this
725     *  property cannot be found
726     */
727    public String getProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
728        return getNestedProperty(bean, name);
729    }
730
731    /**
732     * Gets the <code>PropertyUtilsBean</code> instance used to access properties.
733     *
734     * @return The ConvertUtils bean instance
735     */
736    public PropertyUtilsBean getPropertyUtils() {
737        return propertyUtilsBean;
738    }
739
740    /**
741     * Return the value of the specified simple property of the specified
742     * bean, converted to a String.
743     *
744     * @param bean Bean whose property is to be extracted
745     * @param name Name of the property to be extracted
746     * @return The property's value, converted to a String
747     * @throws IllegalAccessException if the caller does not have
748     *  access to the property accessor method
749     * @throws InvocationTargetException if the property accessor method
750     *  throws an exception
751     * @throws NoSuchMethodException if an accessor method for this
752     *  property cannot be found
753     */
754    public String getSimpleProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
755        final Object value = getPropertyUtils().getSimpleProperty(bean, name);
756        return getConvertUtils().convert(value);
757    }
758
759    /**
760     * If we're running on JDK 1.4 or later, initialize the cause for the given throwable.
761     *
762     * @param  throwable The throwable.
763     * @param  cause     The cause of the throwable.
764     * @return  always true in 1.10.0.
765     * @since 1.8.0
766     * @deprecated Use {@link Throwable#initCause(Throwable)}.
767     * @see Throwable#initCause(Throwable)
768     */
769    @Deprecated
770    public boolean initCause(final Throwable throwable, final Throwable cause) {
771        throwable.initCause(cause);
772        return true;
773    }
774
775    /**
776     * <p>Populate the JavaBeans properties of the specified bean, based on
777     * the specified name/value pairs.  This method uses Java reflection APIs
778     * to identify corresponding "property setter" method names, and deals
779     * with setter arguments of type <code>String</code>, <code>boolean</code>,
780     * <code>int</code>, <code>long</code>, <code>float</code>, and
781     * <code>double</code>.  In addition, array setters for these types (or the
782     * corresponding primitive types) can also be identified.</p>
783     *
784     * <p>The particular setter method to be called for each property is
785     * determined using the usual JavaBeans introspection mechanisms.  Thus,
786     * you may identify custom setter methods using a BeanInfo class that is
787     * associated with the class of the bean itself.  If no such BeanInfo
788     * class is available, the standard method name conversion ("set" plus
789     * the capitalized name of the property in question) is used.</p>
790     *
791     * <p><strong>NOTE</strong>:  It is contrary to the JavaBeans Specification
792     * to have more than one setter method (with different argument
793     * signatures) for the same property.</p>
794     *
795     * <p><strong>WARNING</strong> - The logic of this method is customized
796     * for extracting String-based request parameters from an HTTP request.
797     * It is probably not what you want for general property copying with
798     * type conversion.  For that purpose, check out the
799     * <code>copyProperties()</code> method instead.</p>
800     *
801     * @param bean JavaBean whose properties are being populated
802     * @param properties Map keyed by property name, with the
803     *  corresponding (String or String[]) value(s) to be set
804     *
805     * @throws IllegalAccessException if the caller does not have
806     *  access to the property accessor method
807     * @throws InvocationTargetException if the property accessor method
808     *  throws an exception
809     */
810    public void populate(final Object bean, final Map<String, ? extends Object> properties)
811        throws IllegalAccessException, InvocationTargetException {
812        // Do nothing unless both arguments have been specified
813        if (bean == null || properties == null) {
814            return;
815        }
816        if (log.isDebugEnabled()) {
817            log.debug("BeanUtils.populate(" + bean + ", " +
818                    properties + ")");
819        }
820        // Loop through the property name/value pairs to be set
821        for(final Map.Entry<String, ? extends Object> entry : properties.entrySet()) {
822            // Identify the property name and value(s) to be assigned
823            final String name = entry.getKey();
824            if (name == null) {
825                continue;
826            }
827            // Perform the assignment for this property
828            setProperty(bean, name, entry.getValue());
829        }
830    }
831
832    /**
833     * <p>Set the specified property value, performing type conversions as
834     * required to conform to the type of the destination property.</p>
835     *
836     * <p>If the property is read only then the method returns
837     * without throwing an exception.</p>
838     *
839     * <p>If <code>null</code> is passed into a property expecting a primitive value,
840     * then this will be converted as if it were a <code>null</code> string.</p>
841     *
842     * <p><strong>WARNING</strong> - The logic of this method is customized
843     * to meet the needs of <code>populate()</code>, and is probably not what
844     * you want for general property copying with type conversion.  For that
845     * purpose, check out the <code>copyProperty()</code> method instead.</p>
846     *
847     * <p><strong>WARNING</strong> - PLEASE do not modify the behavior of this
848     * method without consulting with the Struts developer community.  There
849     * are some subtleties to its functionality that are not documented in the
850     * Javadoc description above, yet are vital to the way that Struts utilizes
851     * this method.</p>
852     *
853     * @param bean Bean on which setting is to be performed
854     * @param name Property name (can be nested/indexed/mapped/combo)
855     * @param value Value to be set
856     * @throws IllegalAccessException if the caller does not have
857     *  access to the property accessor method
858     * @throws InvocationTargetException if the property accessor method
859     *  throws an exception
860     */
861    public void setProperty(final Object bean, String name, final Object value) throws IllegalAccessException, InvocationTargetException {
862        // Trace logging (if enabled)
863        if (log.isTraceEnabled()) {
864            final StringBuilder sb = new StringBuilder("  setProperty(");
865            sb.append(bean);
866            sb.append(", ");
867            sb.append(name);
868            sb.append(", ");
869            if (value == null) {
870                sb.append("<NULL>");
871            } else if (value instanceof String) {
872                sb.append((String) value);
873            } else if (value instanceof String[]) {
874                final String[] values = (String[]) value;
875                sb.append('[');
876                for (int i = 0; i < values.length; i++) {
877                    if (i > 0) {
878                        sb.append(',');
879                    }
880                    sb.append(values[i]);
881                }
882                sb.append(']');
883            } else {
884                sb.append(value.toString());
885            }
886            sb.append(')');
887            log.trace(sb.toString());
888        }
889        // Resolve any nested expression to get the actual target bean
890        Object target = bean;
891        final Resolver resolver = getPropertyUtils().getResolver();
892        while (resolver.hasNested(name)) {
893            try {
894                target = getPropertyUtils().getProperty(target, resolver.next(name));
895                if (target == null) { // the value of a nested property is null
896                    return;
897                }
898                name = resolver.remove(name);
899            } catch (final NoSuchMethodException e) {
900                return; // Skip this property setter
901            }
902        }
903        if (log.isTraceEnabled()) {
904            log.trace("    Target bean = " + target);
905            log.trace("    Target name = " + name);
906        }
907        // Declare local variables we will require
908        final String propName = resolver.getProperty(name); // Simple name of target property
909        Class<?> type = null; // Java type of target property
910        final int index = resolver.getIndex(name); // Indexed subscript value (if any)
911        final String key = resolver.getKey(name); // Mapped key value (if any)
912
913        // Calculate the property type
914        if (target instanceof DynaBean) {
915            final DynaClass dynaClass = ((DynaBean) target).getDynaClass();
916            final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
917            if (dynaProperty == null) {
918                return; // Skip this property setter
919            }
920            type = dynaPropertyType(dynaProperty, value);
921            if (index >= 0 && List.class.isAssignableFrom(type)) {
922                type = Object.class;
923            }
924        } else if (target instanceof Map) {
925            type = Object.class;
926        } else if (target != null && target.getClass().isArray() && index >= 0) {
927            type = Array.get(target, index).getClass();
928        } else {
929            PropertyDescriptor descriptor = null;
930            try {
931                descriptor = getPropertyUtils().getPropertyDescriptor(target, name);
932                if (descriptor == null) {
933                    return; // Skip this property setter
934                }
935            } catch (final NoSuchMethodException e) {
936                return; // Skip this property setter
937            }
938            if (descriptor instanceof MappedPropertyDescriptor) {
939                if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) {
940                    if (log.isDebugEnabled()) {
941                        log.debug("Skipping read-only property");
942                    }
943                    return; // Read-only, skip this property setter
944                }
945                type = ((MappedPropertyDescriptor) descriptor).getMappedPropertyType();
946            } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) {
947                if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) {
948                    if (log.isDebugEnabled()) {
949                        log.debug("Skipping read-only property");
950                    }
951                    return; // Read-only, skip this property setter
952                }
953                type = ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType();
954            } else if (index >= 0 && List.class.isAssignableFrom(descriptor.getPropertyType())) {
955                type = Object.class;
956            } else if (key != null) {
957                if (descriptor.getReadMethod() == null) {
958                    if (log.isDebugEnabled()) {
959                        log.debug("Skipping read-only property");
960                    }
961                    return; // Read-only, skip this property setter
962                }
963                type = value == null ? Object.class : value.getClass();
964            } else {
965                if (descriptor.getWriteMethod() == null) {
966                    if (log.isDebugEnabled()) {
967                        log.debug("Skipping read-only property");
968                    }
969                    return; // Read-only, skip this property setter
970                }
971                type = descriptor.getPropertyType();
972            }
973        }
974        // Convert the specified value to the required type
975        Object newValue = null;
976        if (type.isArray() && index < 0) { // Scalar value into array
977            if (value == null) {
978                final String[] values = new String[1];
979                values[0] = null;
980                newValue = getConvertUtils().convert(values, type);
981            } else if (value instanceof String) {
982                newValue = getConvertUtils().convert(value, type);
983            } else if (value instanceof String[]) {
984                newValue = getConvertUtils().convert((String[]) value, type);
985            } else {
986                newValue = convert(value, type);
987            }
988        } else if (type.isArray()) { // Indexed value into array
989            if (value instanceof String || value == null) {
990                newValue = getConvertUtils().convert((String) value, type.getComponentType());
991            } else if (value instanceof String[]) {
992                newValue = getConvertUtils().convert(((String[]) value)[0], type.getComponentType());
993            } else {
994                newValue = convert(value, type.getComponentType());
995            }
996        } else if (value instanceof String) {
997            newValue = getConvertUtils().convert((String) value, type);
998        } else if (value instanceof String[]) {
999            newValue = getConvertUtils().convert(((String[]) value)[0], type);
1000        } else {
1001            newValue = convert(value, type);
1002        }
1003        // Invoke the setter method
1004        try {
1005            getPropertyUtils().setProperty(target, name, newValue);
1006        } catch (final NoSuchMethodException e) {
1007            throw new InvocationTargetException(e, "Cannot set " + propName);
1008        }
1009    }
1010}