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