BeanUtilsBean.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.apache.commons.beanutils2;

  18. import java.beans.IndexedPropertyDescriptor;
  19. import java.beans.PropertyDescriptor;
  20. import java.lang.reflect.Array;
  21. import java.lang.reflect.InvocationTargetException;
  22. import java.util.Collection;
  23. import java.util.HashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Objects;

  27. import org.apache.commons.beanutils2.expression.Resolver;
  28. import org.apache.commons.logging.Log;
  29. import org.apache.commons.logging.LogFactory;

  30. /**
  31.  * TODO docs
  32.  *
  33.  * <p>
  34.  * 2.0
  35.  * </p>
  36.  *
  37.  * <p>
  38.  * {@link BeanUtilsBean} implementation that creates a {@link ConvertUtilsBean} and delegates conversion to {@link ConvertUtilsBean#convert(Object, Class)}.
  39.  * </p>
  40.  *
  41.  * <p>
  42.  * To configure this implementation for the current context ClassLoader invoke {@code BeanUtilsBean.setInstance(new BeanUtilsBean2());}
  43.  * </p>
  44.  *
  45.  * <p>
  46.  * 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
  47.  * to upgrade the {@link Converter} implementations so that they could handle conversion to String for their type (for example IntegerConverter now handles
  48.  * conversion from an Integer to a String as well as String to Integer).
  49.  * </p>
  50.  *
  51.  * <p>
  52.  * In order to take advantage of these improvements BeanUtils needs to change how it gets the appropriate {@link Converter}. This functionality has been
  53.  * implemented in the new {@link ConvertUtilsBean#lookup(Class, Class)} and {@link ConvertUtilsBean#convert(Object, Class)} methods. However changing
  54.  * {@link BeanUtilsBean} to use these methods could create compatibility issues for existing users. In order to avoid that, this new {@link BeanUtilsBean}
  55.  * implementation has been created (and the associated {@link ConvertUtilsBean}).
  56.  * </p>
  57.  *
  58.  * <p>
  59.  * Pre-2.0
  60.  * </p>
  61.  *
  62.  * <p>
  63.  * JavaBean property population methods.
  64.  * </p>
  65.  *
  66.  * <p>
  67.  * This class provides implementations for the utility methods in {@link BeanUtils}. Different instances can be used to isolate caches between class loaders and
  68.  * to vary the value converters registered.
  69.  * </p>
  70.  *
  71.  * @see BeanUtils
  72.  * @since 1.7
  73.  */
  74. public class BeanUtilsBean {

  75.     /**
  76.      * Contains {@code BeanUtilsBean} instances indexed by context classloader.
  77.      */
  78.     private static final ContextClassLoaderLocal<BeanUtilsBean> BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() {
  79.         // Creates the default instance used when the context classloader is unavailable
  80.         @Override
  81.         protected BeanUtilsBean initialValue() {
  82.             return new BeanUtilsBean();
  83.         }
  84.     };

  85.     /**
  86.      * Logging for this instance
  87.      */
  88.     private static final Log LOG = LogFactory.getLog(BeanUtilsBean.class);

  89.     /**
  90.      * Determines the type of a {@code DynaProperty}. Here a special treatment is needed for mapped properties.
  91.      *
  92.      * @param dynaProperty the property descriptor
  93.      * @param value        the value object to be set for this property
  94.      * @return the type of this property
  95.      */
  96.     private static Class<?> dynaPropertyType(final DynaProperty dynaProperty, final Object value) {
  97.         if (!dynaProperty.isMapped()) {
  98.             return dynaProperty.getType();
  99.         }
  100.         return value == null ? String.class : value.getClass();
  101.     }

  102.     /**
  103.      * Gets the instance which provides the functionality for {@link BeanUtils}. This is a pseudo-singleton - an single instance is provided per (thread)
  104.      * context classloader. This mechanism provides isolation for web apps deployed in the same container.
  105.      *
  106.      * @return The (pseudo-singleton) BeanUtils bean instance
  107.      */
  108.     public static BeanUtilsBean getInstance() {
  109.         return BEANS_BY_CLASSLOADER.get();
  110.     }

  111.     /**
  112.      * Sets the instance which provides the functionality for {@link BeanUtils}. This is a pseudo-singleton - an single instance is provided per (thread)
  113.      * context classloader. This mechanism provides isolation for web apps deployed in the same container.
  114.      *
  115.      * @param newInstance The (pseudo-singleton) BeanUtils bean instance
  116.      */
  117.     public static void setInstance(final BeanUtilsBean newInstance) {
  118.         BEANS_BY_CLASSLOADER.set(newInstance);
  119.     }

  120.     /** Used to perform conversions between object types when setting properties */
  121.     private final ConvertUtilsBean convertUtilsBean;

  122.     /** Used to access properties */
  123.     private final PropertyUtilsBean propertyUtilsBean;

  124.     /**
  125.      * <p>
  126.      * Constructs an instance using new property and conversion instances.
  127.      * </p>
  128.      */
  129.     public BeanUtilsBean() {
  130.         this(new ConvertUtilsBean(), new PropertyUtilsBean());
  131.     }

  132.     /**
  133.      * <p>
  134.      * Constructs an instance using given conversion instances and new {@link PropertyUtilsBean} instance.
  135.      * </p>
  136.      *
  137.      * @param todoRemove use this {@code ConvertUtilsBean} to perform conversions from one object to another
  138.      * @since 1.8.0
  139.      */
  140.     public BeanUtilsBean(final ConvertUtilsBean todoRemove) {
  141.         this(new ConvertUtilsBean(), new PropertyUtilsBean());
  142.     }

  143.     /**
  144.      * <p>
  145.      * Constructs an instance using given property and conversion instances.
  146.      * </p>
  147.      *
  148.      * @param convertUtilsBean  use this {@code ConvertUtilsBean} to perform conversions from one object to another
  149.      * @param propertyUtilsBean use this {@code PropertyUtilsBean} to access properties
  150.      */
  151.     public BeanUtilsBean(final ConvertUtilsBean convertUtilsBean, final PropertyUtilsBean propertyUtilsBean) {
  152.         this.convertUtilsBean = convertUtilsBean;
  153.         this.propertyUtilsBean = propertyUtilsBean;
  154.     }

  155.     /**
  156.      * <p>
  157.      * Clone a bean based on the available property getters and setters, even if the bean class itself does not implement Cloneable.
  158.      * </p>
  159.      *
  160.      * <p>
  161.      * <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
  162.      * clone rather than being cloned in turn.
  163.      * </p>
  164.      *
  165.      * @param bean Bean to be cloned
  166.      * @return the cloned bean
  167.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  168.      * @throws InstantiationException    if a new instance of the bean's class cannot be instantiated
  169.      * @throws InvocationTargetException if the property accessor method throws an exception
  170.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  171.      */
  172.     public Object cloneBean(final Object bean) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
  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.      * <p>
  187.      * Converts the value to an object of the specified class (if possible).
  188.      * </p>
  189.      *
  190.      * @param <R> the type of the class for the return value.
  191.      * @param value Value to be converted (may be null)
  192.      * @param type  Class of the value to be converted to
  193.      * @return The converted value
  194.      */
  195.     protected <R> Object convert(final Object value, final Class<R> type) {
  196.         return getConvertUtils().convert(value, type);
  197.     }

  198.     /**
  199.      * 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
  200.      * <strong>null</strong> values are not converted. This causes <strong>null</strong> values to be copied verbatim.
  201.      *
  202.      * @param value the value to be converted and copied
  203.      * @param type  the target type of the conversion
  204.      * @return the converted value
  205.      */
  206.     private Object convertForCopy(final Object value, final Class<?> type) {
  207.         return value != null ? convert(value, type) : value;
  208.     }

  209.     /**
  210.      * <p>
  211.      * 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
  212.      * attempted as necessary. All combinations of standard JavaBeans and DynaBeans as origin and destination are supported. Properties that exist in the origin
  213.      * bean, but do not exist in the destination bean (or are read-only in the destination bean) are silently ignored.
  214.      * </p>
  215.      *
  216.      * <p>
  217.      * 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
  218.      * the corresponding property values that will be converted (if necessary) and set in the destination bean. <strong>Note</strong> that this method is
  219.      * intended to perform a "shallow copy" of the properties and so complex properties (for example, nested ones) will not be copied.
  220.      * </p>
  221.      *
  222.      * <p>
  223.      * This method differs from {@code populate()}, which was primarily designed for populating JavaBeans from the map of request parameters retrieved on an
  224.      * HTTP request, is that no scalar-&gt;indexed or indexed-&gt;scalar manipulations are performed. If the origin property is indexed, the destination
  225.      * property must be also.
  226.      * </p>
  227.      *
  228.      * <p>
  229.      * If you know that no type conversions are required, the {@code copyProperties()} method in {@link PropertyUtils} will execute faster than this method.
  230.      * </p>
  231.      *
  232.      * <p>
  233.      * <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
  234.      * method.
  235.      * </p>
  236.      *
  237.      * @param dest Destination bean whose properties are modified
  238.      * @param orig Origin bean whose properties are retrieved
  239.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  240.      * @throws IllegalArgumentException  if the {@code dest} or {@code orig</code> argument is null or if the <code>dest} property type is different from the
  241.      *                                   source type and the relevant converter has not been registered.
  242.      * @throws InvocationTargetException if the property accessor method throws an exception
  243.      */
  244.     public void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException {
  245.         // Validate existence of the specified beans
  246.         Objects.requireNonNull(dest, "dest");
  247.         Objects.requireNonNull(orig, "orig");
  248.         if (LOG.isDebugEnabled()) {
  249.             LOG.debug("BeanUtils.copyProperties(" + dest + ", " + orig + ")");
  250.         }
  251.         // Copy the properties, converting as necessary
  252.         if (orig instanceof DynaBean) {
  253.             final DynaProperty[] origDescriptors = ((DynaBean) orig).getDynaClass().getDynaProperties();
  254.             for (final DynaProperty origDescriptor : origDescriptors) {
  255.                 final String name = origDescriptor.getName();
  256.                 // Need to check isReadable() for WrapDynaBean
  257.                 // (see Jira issue# BEANUTILS-61)
  258.                 if (getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)) {
  259.                     final Object value = ((DynaBean) orig).get(name);
  260.                     copyProperty(dest, name, value);
  261.                 }
  262.             }
  263.         } else if (orig instanceof Map) {
  264.             @SuppressWarnings("unchecked")
  265.             final
  266.             // Map properties are always of type <String, Object>
  267.             Map<String, Object> propMap = (Map<String, Object>) orig;
  268.             for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
  269.                 final String k = entry.getKey();
  270.                 if (getPropertyUtils().isWriteable(dest, k)) {
  271.                     copyProperty(dest, k, entry.getValue());
  272.                 }
  273.             }
  274.         } else /* if (orig is a standard JavaBean) */ {
  275.             final PropertyDescriptor[] origDescriptors = getPropertyUtils().getPropertyDescriptors(orig);
  276.             for (final PropertyDescriptor origDescriptor : origDescriptors) {
  277.                 final String name = origDescriptor.getName();
  278.                 if ("class".equals(name)) {
  279.                     continue; // No point in trying to set an object's class
  280.                 }
  281.                 if (getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)) {
  282.                     try {
  283.                         final Object value = 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.      * <p>
  294.      * Copy the specified property value to the specified destination bean, performing any type conversion that is required. If the specified bean does not have
  295.      * 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
  296.      * property types, register {@link Converter}s for them by calling the {@code register()} method of {@link ConvertUtils}.
  297.      * </p>
  298.      *
  299.      * <p>
  300.      * <strong>IMPLEMENTATION RESTRICTIONS</strong>:
  301.      * </p>
  302.      * <ul>
  303.      * <li>Does not support destination properties that are indexed, but only an indexed setter (as opposed to an array setter) is available.</li>
  304.      * <li>Does not support destination properties that are mapped, but only a keyed setter (as opposed to a Map setter) is available.</li>
  305.      * <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>
  306.      * </ul>
  307.      *
  308.      * @param bean  Bean on which setting is to be performed
  309.      * @param name  Property name (can be nested/indexed/mapped/combo)
  310.      * @param value Value to be set
  311.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  312.      * @throws InvocationTargetException if the property accessor method throws an exception
  313.      */
  314.     public void copyProperty(final Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException {
  315.         // Trace logging (if enabled)
  316.         if (LOG.isTraceEnabled()) {
  317.             final StringBuilder sb = new StringBuilder("  copyProperty(");
  318.             sb.append(bean);
  319.             sb.append(", ");
  320.             sb.append(name);
  321.             sb.append(", ");
  322.             if (value == null) {
  323.                 sb.append("<NULL>");
  324.             } else if (value instanceof String) {
  325.                 sb.append((String) value);
  326.             } else if (value instanceof String[]) {
  327.                 final String[] values = (String[]) value;
  328.                 sb.append('[');
  329.                 for (int i = 0; i < values.length; i++) {
  330.                     if (i > 0) {
  331.                         sb.append(',');
  332.                     }
  333.                     sb.append(values[i]);
  334.                 }
  335.                 sb.append(']');
  336.             } else {
  337.                 sb.append(value.toString());
  338.             }
  339.             sb.append(')');
  340.             LOG.trace(sb.toString());
  341.         }

  342.         // Resolve any nested expression to get the actual target bean
  343.         Object target = bean;
  344.         final Resolver resolver = getPropertyUtils().getResolver();
  345.         while (resolver.hasNested(name)) {
  346.             try {
  347.                 target = getPropertyUtils().getProperty(target, resolver.next(name));
  348.                 name = resolver.remove(name);
  349.             } catch (final NoSuchMethodException e) {
  350.                 return; // Skip this property setter
  351.             }
  352.         }
  353.         if (LOG.isTraceEnabled()) {
  354.             LOG.trace("    Target bean = " + target);
  355.             LOG.trace("    Target name = " + name);
  356.         }

  357.         // Declare local variables we will require
  358.         final String propName = resolver.getProperty(name); // Simple name of target property
  359.         Class<?> type = null; // Java type of target property
  360.         final int index = resolver.getIndex(name); // Indexed subscript value (if any)
  361.         final String key = resolver.getKey(name); // Mapped key value (if any)

  362.         // Calculate the target property type
  363.         if (target instanceof DynaBean) {
  364.             final DynaClass dynaClass = ((DynaBean) target).getDynaClass();
  365.             final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
  366.             if (dynaProperty == null) {
  367.                 return; // Skip this property setter
  368.             }
  369.             type = dynaPropertyType(dynaProperty, value);
  370.         } else {
  371.             PropertyDescriptor descriptor = null;
  372.             try {
  373.                 descriptor = getPropertyUtils().getPropertyDescriptor(target, name);
  374.                 if (descriptor == null) {
  375.                     return; // Skip this property setter
  376.                 }
  377.             } catch (final NoSuchMethodException e) {
  378.                 return; // Skip this property setter
  379.             }
  380.             type = descriptor.getPropertyType();
  381.             if (type == null) {
  382.                 // Most likely an indexed setter on a POJB only
  383.                 if (LOG.isTraceEnabled()) {
  384.                     LOG.trace("    target type for property '" + propName + "' is null, so skipping the setter");
  385.                 }
  386.                 return;
  387.             }
  388.         }
  389.         if (LOG.isTraceEnabled()) {
  390.             LOG.trace("    target propName=" + propName + ", type=" + type + ", index=" + index + ", key=" + key);
  391.         }

  392.         // Convert the specified value to the required type and store it
  393.         if (index >= 0) { // Destination must be indexed
  394.             value = convertForCopy(value, type.getComponentType());
  395.             try {
  396.                 getPropertyUtils().setIndexedProperty(target, propName, index, value);
  397.             } catch (final NoSuchMethodException e) {
  398.                 throw new InvocationTargetException(e, "Cannot set " + propName);
  399.             }
  400.         } else if (key != null) { // Destination must be mapped
  401.             // Maps do not know what the preferred data type is,
  402.             // so perform no conversions at all
  403.             // FIXME - should we create or support a TypedMap?
  404.             try {
  405.                 getPropertyUtils().setMappedProperty(target, propName, key, value);
  406.             } catch (final NoSuchMethodException e) {
  407.                 throw new InvocationTargetException(e, "Cannot set " + propName);
  408.             }
  409.         } else { // Destination must be simple
  410.             value = convertForCopy(value, type);
  411.             try {
  412.                 getPropertyUtils().setSimpleProperty(target, propName, value);
  413.             } catch (final NoSuchMethodException e) {
  414.                 throw new InvocationTargetException(e, "Cannot set " + propName);
  415.             }
  416.         }
  417.     }

  418.     /**
  419.      * <p>
  420.      * Return the entire set of properties for which the specified bean provides a read method. This map contains the to {@code String} converted property
  421.      * values for all properties for which a read method is provided (i.e. where the getReadMethod() returns non-null).
  422.      * </p>
  423.      *
  424.      * <p>
  425.      * 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
  426.      * write-only properties, but only if there are no indexed properties.
  427.      * </p>
  428.      *
  429.      * <p>
  430.      * <strong>Warning:</strong> if any of the bean property implementations contain (directly or indirectly) a call to this method then a stack overflow may
  431.      * result. For example:
  432.      * </p>
  433.      *
  434.      * <pre>
  435.      * <code>
  436.      * class MyBean
  437.      * {
  438.      *    public Map getParameterMap()
  439.      *    {
  440.      *         BeanUtils.describe(this);
  441.      *    }
  442.      * }
  443.      * </code>
  444.      * </pre>
  445.      * <p>
  446.      * will result in an infinite regression when {@code getParametersMap} is called. It is recommended that such methods are given alternative names (for
  447.      * example, {@code parametersMap}).
  448.      * </p>
  449.      *
  450.      * @param bean Bean whose properties are to be extracted
  451.      * @return Map of property descriptors
  452.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  453.      * @throws InvocationTargetException if the property accessor method throws an exception
  454.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  455.      */
  456.     public Map<String, String> describe(final Object bean) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  457.         if (bean == null) {
  458.             // return (Collections.EMPTY_MAP);
  459.             return new java.util.HashMap<>();
  460.         }

  461.         if (LOG.isDebugEnabled()) {
  462.             LOG.debug("Describing bean: " + bean.getClass().getName());
  463.         }

  464.         final Map<String, String> description = new HashMap<>();
  465.         if (bean instanceof DynaBean) {
  466.             final DynaProperty[] descriptors = ((DynaBean) bean).getDynaClass().getDynaProperties();
  467.             for (final DynaProperty descriptor : descriptors) {
  468.                 final String name = descriptor.getName();
  469.                 description.put(name, getProperty(bean, name));
  470.             }
  471.         } else {
  472.             final PropertyDescriptor[] descriptors = getPropertyUtils().getPropertyDescriptors(bean);
  473.             final Class<?> clazz = bean.getClass();
  474.             for (final PropertyDescriptor descriptor : descriptors) {
  475.                 final String name = descriptor.getName();
  476.                 if (getPropertyUtils().getReadMethod(clazz, descriptor) != null) {
  477.                     description.put(name, getProperty(bean, name));
  478.                 }
  479.             }
  480.         }
  481.         return description;
  482.     }

  483.     /**
  484.      * Gets the value of the specified array property of the specified bean, as a String array.
  485.      *
  486.      * @param bean Bean whose property is to be extracted
  487.      * @param name Name of the property to be extracted
  488.      * @return The array property value
  489.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  490.      * @throws InvocationTargetException if the property accessor method throws an exception
  491.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  492.      */
  493.     public String[] getArrayProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  494.         final Object value = getPropertyUtils().getProperty(bean, name);
  495.         if (value == null) {
  496.             return null;
  497.         }
  498.         if (value instanceof Collection) {
  499.             return ((Collection<?>) value).stream().map(item -> item != null ? getConvertUtils().convert(item) : null).toArray(String[]::new);
  500.         }
  501.         if (!value.getClass().isArray()) {
  502.             final String[] results = new String[1];
  503.             results[0] = getConvertUtils().convert(value);
  504.             return results;
  505.         }
  506.         final int n = Array.getLength(value);
  507.         final String[] results = new String[n];
  508.         for (int i = 0; i < n; i++) {
  509.             final Object item = Array.get(value, i);
  510.             if (item == null) {
  511.                 results[i] = null;
  512.             } else {
  513.                 // convert to string using convert utils
  514.                 results[i] = getConvertUtils().convert(item);
  515.             }
  516.         }
  517.         return results;
  518.     }

  519.     /**
  520.      * Gets the {@code ConvertUtilsBean} instance used to perform the conversions.
  521.      *
  522.      * @return The ConvertUtils bean instance
  523.      */
  524.     public ConvertUtilsBean getConvertUtils() {
  525.         return convertUtilsBean;
  526.     }

  527.     /**
  528.      * 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
  529.      * square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown.
  530.      *
  531.      * @param bean Bean whose property is to be extracted
  532.      * @param name {@code propertyname[index]} of the property value to be extracted
  533.      * @return The indexed property's value, converted to a String
  534.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  535.      * @throws InvocationTargetException if the property accessor method throws an exception
  536.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  537.      */
  538.     public String getIndexedProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  539.         final Object value = getPropertyUtils().getIndexedProperty(bean, name);
  540.         return getConvertUtils().convert(value);
  541.     }

  542.     /**
  543.      * 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
  544.      * included in the property name expression
  545.      *
  546.      * @param bean  Bean whose property is to be extracted
  547.      * @param name  Simple property name of the property value to be extracted
  548.      * @param index Index of the property value to be extracted
  549.      * @return The indexed property's value, converted to a String
  550.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  551.      * @throws InvocationTargetException if the property accessor method throws an exception
  552.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  553.      */
  554.     public String getIndexedProperty(final Object bean, final String name, final int index)
  555.             throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  556.         final Object value = getPropertyUtils().getIndexedProperty(bean, name, index);
  557.         return getConvertUtils().convert(value);
  558.     }

  559.     /**
  560.      * 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
  561.      * parentheses) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown.
  562.      *
  563.      * @param bean Bean whose property is to be extracted
  564.      * @param name {@code propertyname(index)} of the property value to be extracted
  565.      * @return The mapped property's value, converted to a String
  566.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  567.      * @throws InvocationTargetException if the property accessor method throws an exception
  568.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  569.      */
  570.     public String getMappedProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  571.         final Object value = getPropertyUtils().getMappedProperty(bean, name);
  572.         return getConvertUtils().convert(value);
  573.     }

  574.     /**
  575.      * 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
  576.      * in the property name expression
  577.      *
  578.      * @param bean Bean whose property is to be extracted
  579.      * @param name Simple property name of the property value to be extracted
  580.      * @param key  Lookup key of the property value to be extracted
  581.      * @return The mapped property's value, converted to a String
  582.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  583.      * @throws InvocationTargetException if the property accessor method throws an exception
  584.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  585.      */
  586.     public String getMappedProperty(final Object bean, final String name, final String key)
  587.             throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  588.         final Object value = getPropertyUtils().getMappedProperty(bean, name, key);
  589.         return getConvertUtils().convert(value);
  590.     }

  591.     /**
  592.      * Gets the value of the (possibly nested) property of the specified name, for the specified bean, as a String.
  593.      *
  594.      * @param bean Bean whose property is to be extracted
  595.      * @param name Possibly nested name of the property to be extracted
  596.      * @return The nested property's value, converted to a String
  597.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  598.      * @throws IllegalArgumentException  if a nested reference to a property returns null
  599.      * @throws InvocationTargetException if the property accessor method throws an exception
  600.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  601.      */
  602.     public String getNestedProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  603.         final Object value = getPropertyUtils().getNestedProperty(bean, name);
  604.         return getConvertUtils().convert(value);
  605.     }

  606.     /**
  607.      * Gets the value of the specified property of the specified bean, no matter which property reference format is used, as a String.
  608.      *
  609.      * @param bean Bean whose property is to be extracted
  610.      * @param name Possibly indexed and/or nested name of the property to be extracted
  611.      * @return The property's value, converted to a String
  612.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  613.      * @throws InvocationTargetException if the property accessor method throws an exception
  614.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  615.      */
  616.     public String getProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  617.         return getNestedProperty(bean, name);
  618.     }

  619.     /**
  620.      * Gets the {@code PropertyUtilsBean} instance used to access properties.
  621.      *
  622.      * @return The ConvertUtils bean instance
  623.      */
  624.     public PropertyUtilsBean getPropertyUtils() {
  625.         return propertyUtilsBean;
  626.     }

  627.     /**
  628.      * Gets the value of the specified simple property of the specified bean, converted to a String.
  629.      *
  630.      * @param bean Bean whose property is to be extracted
  631.      * @param name Name of the property to be extracted
  632.      * @return The property's value, converted to a String
  633.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  634.      * @throws InvocationTargetException if the property accessor method throws an exception
  635.      * @throws NoSuchMethodException     if an accessor method for this property cannot be found
  636.      */
  637.     public String getSimpleProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  638.         final Object value = getPropertyUtils().getSimpleProperty(bean, name);
  639.         return getConvertUtils().convert(value);
  640.     }

  641.     /**
  642.      * <p>
  643.      * Populate the JavaBeans properties of the specified bean, based on the specified name/value pairs. This method uses Java reflection APIs to identify
  644.      * corresponding "property setter" method names, and deals with setter arguments of type {@code String</code>, <code>boolean}, {@code int}, {@code long},
  645.      * {@code float}, and {@code double}. In addition, array setters for these types (or the corresponding primitive types) can also be identified.
  646.      * </p>
  647.      *
  648.      * <p>
  649.      * The particular setter method to be called for each property is determined using the usual JavaBeans introspection mechanisms. Thus, you may identify
  650.      * 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
  651.      * method name conversion ("set" plus the capitalized name of the property in question) is used.
  652.      * </p>
  653.      *
  654.      * <p>
  655.      * <strong>NOTE</strong>: It is contrary to the JavaBeans Specification to have more than one setter method (with different argument signatures) for the
  656.      * same property.
  657.      * </p>
  658.      *
  659.      * <p>
  660.      * <strong>WARNING</strong> - The logic of this method is customized for extracting String-based request parameters from an HTTP request. It is probably not
  661.      * what you want for general property copying with type conversion. For that purpose, check out the {@code copyProperties()} method instead.
  662.      * </p>
  663.      *
  664.      * @param bean       JavaBean whose properties are being populated
  665.      * @param properties Map keyed by property name, with the corresponding (String or String[]) value(s) to be set
  666.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  667.      * @throws InvocationTargetException if the property accessor method throws an exception
  668.      */
  669.     public void populate(final Object bean, final Map<String, ? extends Object> properties) throws IllegalAccessException, InvocationTargetException {
  670.         // Do nothing unless both arguments have been specified
  671.         if (bean == null || properties == null) {
  672.             return;
  673.         }
  674.         if (LOG.isDebugEnabled()) {
  675.             LOG.debug("BeanUtils.populate(" + bean + ", " + properties + ")");
  676.         }

  677.         // Loop through the property name/value pairs to be set
  678.         for (final Map.Entry<String, ? extends Object> entry : properties.entrySet()) {
  679.             // Identify the property name and value(s) to be assigned
  680.             final String name = entry.getKey();
  681.             if (name == null) {
  682.                 continue;
  683.             }

  684.             // Perform the assignment for this property
  685.             setProperty(bean, name, entry.getValue());
  686.         }
  687.     }

  688.     /**
  689.      * <p>
  690.      * Set the specified property value, performing type conversions as required to conform to the type of the destination property.
  691.      * </p>
  692.      *
  693.      * <p>
  694.      * If the property is read only then the method returns without throwing an exception.
  695.      * </p>
  696.      *
  697.      * <p>
  698.      * 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.
  699.      * </p>
  700.      *
  701.      * <p>
  702.      * <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
  703.      * property copying with type conversion. For that purpose, check out the {@code copyProperty()} method instead.
  704.      * </p>
  705.      *
  706.      * <p>
  707.      * <strong>WARNING</strong> - PLEASE do not modify the behavior of this method without consulting with the Struts developer community. There are some
  708.      * subtleties to its functionality that are not documented in the Javadoc description above, yet are vital to the way that Struts utilizes this method.
  709.      * </p>
  710.      *
  711.      * @param bean  Bean on which setting is to be performed
  712.      * @param name  Property name (can be nested/indexed/mapped/combo)
  713.      * @param value Value to be set
  714.      * @throws IllegalAccessException    if the caller does not have access to the property accessor method
  715.      * @throws InvocationTargetException if the property accessor method throws an exception
  716.      */
  717.     public void setProperty(final Object bean, String name, final Object value) throws IllegalAccessException, InvocationTargetException {
  718.         // Trace logging (if enabled)
  719.         if (LOG.isTraceEnabled()) {
  720.             final StringBuilder sb = new StringBuilder("  setProperty(");
  721.             sb.append(bean);
  722.             sb.append(", ");
  723.             sb.append(name);
  724.             sb.append(", ");
  725.             if (value == null) {
  726.                 sb.append("<NULL>");
  727.             } else if (value instanceof String) {
  728.                 sb.append((String) value);
  729.             } else if (value instanceof String[]) {
  730.                 final String[] values = (String[]) value;
  731.                 sb.append('[');
  732.                 for (int i = 0; i < values.length; i++) {
  733.                     if (i > 0) {
  734.                         sb.append(',');
  735.                     }
  736.                     sb.append(values[i]);
  737.                 }
  738.                 sb.append(']');
  739.             } else {
  740.                 sb.append(value.toString());
  741.             }
  742.             sb.append(')');
  743.             LOG.trace(sb.toString());
  744.         }

  745.         // Resolve any nested expression to get the actual target bean
  746.         Object target = bean;
  747.         final Resolver resolver = getPropertyUtils().getResolver();
  748.         while (resolver.hasNested(name)) {
  749.             try {
  750.                 target = getPropertyUtils().getProperty(target, resolver.next(name));
  751.                 if (target == null) { // the value of a nested property is null
  752.                     return;
  753.                 }
  754.                 name = resolver.remove(name);
  755.             } catch (final NoSuchMethodException e) {
  756.                 return; // Skip this property setter
  757.             }
  758.         }
  759.         if (LOG.isTraceEnabled()) {
  760.             LOG.trace("    Target bean = " + target);
  761.             LOG.trace("    Target name = " + name);
  762.         }

  763.         // Declare local variables we will require
  764.         final String propName = resolver.getProperty(name); // Simple name of target property
  765.         Class<?> type = null; // Java type of target property
  766.         final int index = resolver.getIndex(name); // Indexed subscript value (if any)
  767.         final String key = resolver.getKey(name); // Mapped key value (if any)

  768.         // Calculate the property type
  769.         if (target instanceof DynaBean) {
  770.             final DynaClass dynaClass = ((DynaBean) target).getDynaClass();
  771.             final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
  772.             if (dynaProperty == null) {
  773.                 return; // Skip this property setter
  774.             }
  775.             type = dynaPropertyType(dynaProperty, value);
  776.             if (index >= 0 && List.class.isAssignableFrom(type)) {
  777.                 type = Object.class;
  778.             }
  779.         } else if (target instanceof Map) {
  780.             type = Object.class;
  781.         } else if (target != null && target.getClass().isArray() && index >= 0) {
  782.             type = Array.get(target, index).getClass();
  783.         } else {
  784.             PropertyDescriptor descriptor = null;
  785.             try {
  786.                 descriptor = getPropertyUtils().getPropertyDescriptor(target, name);
  787.                 if (descriptor == null) {
  788.                     return; // Skip this property setter
  789.                 }
  790.             } catch (final NoSuchMethodException e) {
  791.                 return; // Skip this property setter
  792.             }
  793.             if (descriptor instanceof MappedPropertyDescriptor) {
  794.                 if (((MappedPropertyDescriptor) descriptor).getMappedWriteMethod() == null) {
  795.                     if (LOG.isDebugEnabled()) {
  796.                         LOG.debug("Skipping read-only property");
  797.                     }
  798.                     return; // Read-only, skip this property setter
  799.                 }
  800.                 type = ((MappedPropertyDescriptor) descriptor).getMappedPropertyType();
  801.             } else if (index >= 0 && descriptor instanceof IndexedPropertyDescriptor) {
  802.                 if (((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod() == null) {
  803.                     if (LOG.isDebugEnabled()) {
  804.                         LOG.debug("Skipping read-only property");
  805.                     }
  806.                     return; // Read-only, skip this property setter
  807.                 }
  808.                 type = ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType();
  809.             } else if (index >= 0 && List.class.isAssignableFrom(descriptor.getPropertyType())) {
  810.                 type = Object.class;
  811.             } else if (key != null) {
  812.                 if (descriptor.getReadMethod() == null) {
  813.                     if (LOG.isDebugEnabled()) {
  814.                         LOG.debug("Skipping read-only property");
  815.                     }
  816.                     return; // Read-only, skip this property setter
  817.                 }
  818.                 type = value == null ? Object.class : value.getClass();
  819.             } else {
  820.                 if (descriptor.getWriteMethod() == null) {
  821.                     if (LOG.isDebugEnabled()) {
  822.                         LOG.debug("Skipping read-only property");
  823.                     }
  824.                     return; // Read-only, skip this property setter
  825.                 }
  826.                 type = descriptor.getPropertyType();
  827.             }
  828.         }

  829.         // Convert the specified value to the required type
  830.         Object newValue = null;
  831.         if (type.isArray() && index < 0) { // Scalar value into array
  832.             if (value == null) {
  833.                 final String[] values = new String[1];
  834.                 values[0] = null;
  835.                 newValue = getConvertUtils().convert(values, type);
  836.             } else if (value instanceof String) {
  837.                 newValue = getConvertUtils().convert(value, type);
  838.             } else if (value instanceof String[]) {
  839.                 newValue = getConvertUtils().convert((String[]) value, type);
  840.             } else {
  841.                 newValue = convert(value, type);
  842.             }
  843.         } else if (type.isArray()) { // Indexed value into array
  844.             if (value instanceof String || value == null) {
  845.                 newValue = getConvertUtils().convert((String) value, type.getComponentType());
  846.             } else if (value instanceof String[]) {
  847.                 newValue = getConvertUtils().convert(((String[]) value)[0], type.getComponentType());
  848.             } else {
  849.                 newValue = convert(value, type.getComponentType());
  850.             }
  851.         } else if (value instanceof String) {
  852.             newValue = getConvertUtils().convert((String) value, type);
  853.         } else if (value instanceof String[]) {
  854.             newValue = getConvertUtils().convert(((String[]) value)[0], type);
  855.         } else {
  856.             newValue = convert(value, type);
  857.         }

  858.         // Invoke the setter method
  859.         try {
  860.             getPropertyUtils().setProperty(target, name, newValue);
  861.         } catch (final NoSuchMethodException e) {
  862.             throw new InvocationTargetException(e, "Cannot set " + propName);
  863.         }
  864.     }
  865. }