    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018
    019package org.apache.commons.beanutils;
    020
    021
    022import java.beans.IndexedPropertyDescriptor;
    023import java.beans.PropertyDescriptor;
    024import java.lang.reflect.Array;
    025import java.lang.reflect.InvocationTargetException;
    026import java.lang.reflect.Method;
    027import java.util.ArrayList;
    028import java.util.Collection;
    029import java.util.HashMap;
    030import java.util.List;
    031import java.util.Map;
    032
    033import org.apache.commons.beanutils.expression.Resolver;
    034import org.apache.commons.logging.Log;
    035import org.apache.commons.logging.LogFactory;
    036
    037
    038/**
    039 * <p>JavaBean property population methods.</p>
    040 *
    041 * <p>This class provides implementations for the utility methods in
    042 * {@link BeanUtils}.
    043 * Different instances can be used to isolate caches between classloaders
    044 * and to vary the value converters registered.</p>
    045 *
    046 * @version $Id$
    047 * @see BeanUtils
    048 * @since 1.7
    049 */
    050
    051public class BeanUtilsBean {
    052
    053
    054    // ------------------------------------------------------ Private Class Variables
    055
    056    /**
    057     * Contains <code>BeanUtilsBean</code> instances indexed by context classloader.
    058     */
    059    private static final ContextClassLoaderLocal<BeanUtilsBean>
    060            BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
    061                        // Creates the default instance used when the context classloader is unavailable
    062                        @Override
    063                        protected BeanUtilsBean initialValue() {
    064                            return new BeanUtilsBean();
    065                        }
    066                    };
    067
    068    /**
    069     * Gets the instance which provides the functionality for {@link BeanUtils}.
    070     * This is a pseudo-singleton - an single instance is provided per (thread) context classloader.
    071     * This mechanism provides isolation for web apps deployed in the same container.
    072     *
    073     * @return The (pseudo-singleton) BeanUtils bean instance
    074     */
    075    public static BeanUtilsBean getInstance() {
    076        return BEANS_BY_CLASSLOADER.get();
    077    }
    078
    079    /**
    080     * Sets the instance which provides the functionality for {@link BeanUtils}.
    081     * This is a pseudo-singleton - an single instance is provided per (thread) context classloader.
    082     * This mechanism provides isolation for web apps deployed in the same container.
    083     *
    084     * @param newInstance The (pseudo-singleton) BeanUtils bean instance
    085     */
    086    public static void setInstance(final BeanUtilsBean newInstance) {
    087        BEANS_BY_CLASSLOADER.set(newInstance);
    088    }
    089
    090    // --------------------------------------------------------- Attributes
    091
    092    /**
    093     * Logging for this instance
    094     */
    095    private final Log log = LogFactory.getLog(BeanUtils.class);
    096
    097    /** Used to perform conversions between object types when setting properties */
    098    private final ConvertUtilsBean convertUtilsBean;
    099
    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}