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.IntrospectionException;
022import java.beans.Introspector;
023import java.beans.PropertyDescriptor;
024import java.lang.reflect.Array;
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.Method;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.CopyOnWriteArrayList;
033
034import org.apache.commons.beanutils2.expression.DefaultResolver;
035import org.apache.commons.beanutils2.expression.Resolver;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038
039/**
040 * Utility methods for using Java Reflection APIs to facilitate generic property getter and setter operations on Java objects. Much of this code was originally
041 * included in {@code BeanUtils}, but has been separated because of the volume of code involved.
042 * <p>
043 * In general, the objects that are examined and modified using these methods are expected to conform to the property getter and setter method naming
044 * conventions described in the JavaBeans Specification (Version 1.0.1). No data type conversions are performed, and there are no usage of any
045 * {@code PropertyEditor} classes that have been registered, although a convenient way to access the registered classes themselves is included.
046 * <p>
047 * For the purposes of this class, five formats for referencing a particular property value of a bean are defined, with the <em>default</em> layout of an
048 * identifying String in parentheses. However the notation for these formats and how they are resolved is now (since BeanUtils 1.8.0) controlled by the
049 * configured {@link Resolver} implementation:
050 * <ul>
051 * <li><strong>Simple ({@code name})</strong> - The specified {@code name} identifies an individual property of a particular JavaBean. The name of the actual
052 * getter or setter method to be used is determined using standard JavaBeans introspection, so that (unless overridden by a {@code BeanInfo} class, a property
053 * named "xyz" will have a getter method named {@code getXyz()} or (for boolean properties only) {@code isXyz()}, and a setter method named
054 * {@code setXyz()}.</li>
055 * <li><strong>Nested ({@code name1.name2.name3})</strong> The first name element is used to select a property getter, as for simple references above. The
056 * object returned for this property is then consulted, using the same approach, for a property getter for a property named {@code name2}, and so on. The
057 * property value that is ultimately retrieved or modified is the one identified by the last name element.</li>
058 * <li><strong>Indexed ({@code name[index]})</strong> - The underlying property value is assumed to be an array, or this JavaBean is assumed to have indexed
059 * property getter and setter methods. The appropriate (zero-relative) entry in the array is selected. {@code List} objects are now also supported for
060 * read/write. You simply need to define a getter that returns the {@code List}</li>
061 * <li><strong>Mapped ({@code name(key)})</strong> - The JavaBean is assumed to have an property getter and setter methods with an additional attribute of type
062 * {@link String}.</li>
063 * <li><strong>Combined ({@code name1.name2[index].name3(key)})</strong> - Combining mapped, nested, and indexed references is also supported.</li>
064 * </ul>
065 *
066 * @see Resolver
067 * @see PropertyUtils
068 * @since 1.7
069 */
070public class PropertyUtilsBean {
071
072    /** Log instance */
073    private static final Log LOG = LogFactory.getLog(PropertyUtilsBean.class);
074
075    /**
076     * Gets the PropertyUtils bean instance.
077     *
078     * @return The PropertyUtils bean instance
079     */
080    protected static PropertyUtilsBean getInstance() {
081        return BeanUtilsBean.getInstance().getPropertyUtils();
082    }
083
084    /**
085     * Converts an object to a list of objects. This method is used when dealing with indexed properties. It assumes that indexed properties are stored as lists
086     * of objects.
087     *
088     * @param obj the object to be converted
089     * @return the resulting list of objects
090     */
091    @SuppressWarnings("unchecked")
092    private static List<Object> toObjectList(final Object obj) {
093        // indexed properties are stored in lists of objects
094        return (List<Object>) obj;
095    }
096
097    /**
098     * Converts an object to a map with property values. This method is used when dealing with mapped properties. It assumes that mapped properties are stored
099     * in a Map&lt;String, Object&gt;.
100     *
101     * @param obj the object to be converted
102     * @return the resulting properties map
103     */
104    @SuppressWarnings("unchecked")
105    private static Map<String, Object> toPropertyMap(final Object obj) {
106        // mapped properties are stores in maps of type <String, Object>
107        return (Map<String, Object>) obj;
108    }
109
110    private Resolver resolver = new DefaultResolver();
111
112    /**
113     * The cache of PropertyDescriptor arrays for beans we have already introspected, keyed by the java.lang.Class of this object.
114     */
115    private final Map<Class<?>, BeanIntrospectionData> descriptorsCache;
116
117    private final Map<Class<?>, Map> mappedDescriptorsCache;
118
119    /** The list with BeanIntrospector objects. */
120    private final List<BeanIntrospector> introspectors;
121
122    /** Base constructor */
123    public PropertyUtilsBean() {
124        descriptorsCache = BeanUtils.createCache();
125        mappedDescriptorsCache = BeanUtils.createCache();
126        introspectors = new CopyOnWriteArrayList<>();
127        resetBeanIntrospectors();
128    }
129
130    /**
131     * Adds a {@code BeanIntrospector}. This object is invoked when the property descriptors of a class need to be obtained.
132     *
133     * @param introspector the {@code BeanIntrospector} to be added (must not be <strong>null</strong>
134     * @throws IllegalArgumentException if the argument is <strong>null</strong>
135     * @since 1.9
136     */
137    public void addBeanIntrospector(final BeanIntrospector introspector) {
138        introspectors.add(Objects.requireNonNull(introspector, "introspector"));
139    }
140
141    /**
142     * Clear any cached property descriptors information for all classes loaded by any class loaders. This is useful in cases where class loaders are thrown
143     * away to implement class reloading.
144     */
145    public void clearDescriptors() {
146        descriptorsCache.clear();
147        mappedDescriptorsCache.clear();
148        Introspector.flushCaches();
149    }
150
151    /**
152     * <p>
153     * Copy property values from the "origin" bean to the "destination" bean for all cases where the property names are the same (even though the actual getter
154     * and setter methods might have been customized via {@code BeanInfo} classes). No conversions are performed on the actual property values -- it is assumed
155     * that the values retrieved from the origin bean are assignment-compatible with the types expected by the destination bean.
156     * </p>
157     *
158     * <p>
159     * 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
160     * the corresponding property values that will be set in the destination bean.<strong>Note</strong> that this method is intended to perform a "shallow copy"
161     * of the properties and so complex properties (for example, nested ones) will not be copied.
162     * </p>
163     *
164     * <p>
165     * Note, that this method will not copy a List to a List, or an Object[] to an Object[]. It's specifically for copying JavaBean properties.
166     * </p>
167     *
168     * @param dest Destination bean whose properties are modified
169     * @param orig Origin bean whose properties are retrieved
170     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
171     * @throws IllegalArgumentException  if the {@code dest} or {@code orig} argument is null
172     * @throws InvocationTargetException if the property accessor method throws an exception
173     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
174     */
175    public void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException,
176            // TODO BEFORE 2.0
177            // MISMATCH between implementation and Javadoc.
178            NoSuchMethodException {
179        Objects.requireNonNull(dest, "dest");
180        Objects.requireNonNull(orig, "orig");
181        if (orig instanceof DynaBean) {
182            final DynaProperty[] origDescriptors = ((DynaBean) orig).getDynaClass().getDynaProperties();
183            for (final DynaProperty origDescriptor : origDescriptors) {
184                final String name = origDescriptor.getName();
185                if (isReadable(orig, name) && isWriteable(dest, name)) {
186                    try {
187                        final Object value = ((DynaBean) orig).get(name);
188                        if (dest instanceof DynaBean) {
189                            ((DynaBean) dest).set(name, value);
190                        } else {
191                            setSimpleProperty(dest, name, value);
192                        }
193                    } catch (final NoSuchMethodException e) {
194                        if (LOG.isDebugEnabled()) {
195                            LOG.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
196                        }
197                    }
198                }
199            }
200        } else if (orig instanceof Map) {
201            for (final Map.Entry<?, ?> entry : ((Map<?, ?>) orig).entrySet()) {
202                final String name = (String) entry.getKey();
203                if (isWriteable(dest, name)) {
204                    try {
205                        if (dest instanceof DynaBean) {
206                            ((DynaBean) dest).set(name, entry.getValue());
207                        } else {
208                            setSimpleProperty(dest, name, entry.getValue());
209                        }
210                    } catch (final NoSuchMethodException e) {
211                        if (LOG.isDebugEnabled()) {
212                            LOG.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
213                        }
214                    }
215                }
216            }
217        } else /* if (orig is a standard JavaBean) */ {
218            final PropertyDescriptor[] origDescriptors = getPropertyDescriptors(orig);
219            for (final PropertyDescriptor origDescriptor : origDescriptors) {
220                final String name = origDescriptor.getName();
221                if (isReadable(orig, name) && isWriteable(dest, name)) {
222                    try {
223                        final Object value = getSimpleProperty(orig, name);
224                        if (dest instanceof DynaBean) {
225                            ((DynaBean) dest).set(name, value);
226                        } else {
227                            setSimpleProperty(dest, name, value);
228                        }
229                    } catch (final NoSuchMethodException e) {
230                        if (LOG.isDebugEnabled()) {
231                            LOG.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", e);
232                        }
233                    }
234                }
235            }
236        }
237
238    }
239
240    /**
241     * <p>
242     * Return the entire set of properties for which the specified bean provides a read method. This map contains the unconverted property values for all
243     * properties for which a read method is provided (i.e. where the {@code getReadMethod()} returns non-null).
244     * </p>
245     *
246     * <p>
247     * <strong>FIXME</strong> - Does not account for mapped properties.
248     * </p>
249     *
250     * @param bean Bean whose properties are to be extracted
251     * @return The set of properties for the bean
252     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
253     * @throws IllegalArgumentException  if {@code bean} is null
254     * @throws InvocationTargetException if the property accessor method throws an exception
255     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
256     */
257    public Map<String, Object> describe(final Object bean) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
258        Objects.requireNonNull(bean, "bean");
259        final Map<String, Object> description = new HashMap<>();
260        if (bean instanceof DynaBean) {
261            final DynaProperty[] descriptors = ((DynaBean) bean).getDynaClass().getDynaProperties();
262            for (final DynaProperty descriptor : descriptors) {
263                final String name = descriptor.getName();
264                description.put(name, getProperty(bean, name));
265            }
266        } else {
267            final PropertyDescriptor[] descriptors = getPropertyDescriptors(bean);
268            for (final PropertyDescriptor descriptor : descriptors) {
269                final String name = descriptor.getName();
270                if (descriptor.getReadMethod() != null) {
271                    description.put(name, getProperty(bean, name));
272                }
273            }
274        }
275        return description;
276    }
277
278    /**
279     * Performs introspection on the specified class. This method invokes all {@code BeanIntrospector} objects that were added to this instance.
280     *
281     * @param beanClass the class to be inspected
282     * @return a data object with the results of introspection
283     */
284    private BeanIntrospectionData fetchIntrospectionData(final Class<?> beanClass) {
285        final DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass);
286
287        for (final BeanIntrospector bi : introspectors) {
288            try {
289                bi.introspect(ictx);
290            } catch (final IntrospectionException iex) {
291                LOG.error("Exception during introspection", iex);
292            }
293        }
294
295        return new BeanIntrospectionData(ictx.getPropertyDescriptors());
296    }
297
298    /**
299     * Gets the value of the specified indexed property of the specified bean, with no type conversions. The zero-relative index of the required value must be
300     * included (in square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. In addition to supporting the
301     * JavaBeans specification, this method has been extended to support {@code List} objects as well.
302     *
303     * @param bean Bean whose property is to be extracted
304     * @param name {@code propertyname[index]} of the property value to be extracted
305     * @return the indexed property value
306     * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying array or List
307     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
308     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
309     * @throws InvocationTargetException if the property accessor method throws an exception
310     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
311     */
312    public Object getIndexedProperty(final Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
313        Objects.requireNonNull(bean, "bean");
314        Objects.requireNonNull(name, "name");
315        // Identify the index of the requested individual property
316        int index = -1;
317        try {
318            index = resolver.getIndex(name);
319        } catch (final IllegalArgumentException e) {
320            throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "' " + e.getMessage());
321        }
322        if (index < 0) {
323            throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'");
324        }
325
326        // Isolate the name
327        name = resolver.getProperty(name);
328
329        // Request the specified indexed property value
330        return getIndexedProperty(bean, name, index);
331    }
332
333    /**
334     * Gets the value of the specified indexed property of the specified bean, with no type conversions. In addition to supporting the JavaBeans specification,
335     * this method has been extended to support {@code List} objects as well.
336     *
337     * @param bean  Bean whose property is to be extracted
338     * @param name  Simple property name of the property value to be extracted
339     * @param index Index of the property value to be extracted
340     * @return the indexed property value
341     * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property
342     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
343     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
344     * @throws InvocationTargetException if the property accessor method throws an exception
345     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
346     */
347    public Object getIndexedProperty(final Object bean, final String name, final int index)
348            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
349        Objects.requireNonNull(bean, "bean");
350        if (name == null || name.isEmpty()) {
351            if (bean.getClass().isArray()) {
352                return Array.get(bean, index);
353            }
354            if (bean instanceof List) {
355                return ((List<?>) bean).get(index);
356            }
357        }
358        Objects.requireNonNull(name, "name");
359        // Handle DynaBean instances specially
360        if (bean instanceof DynaBean) {
361            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
362            if (descriptor == null) {
363                throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
364            }
365            return ((DynaBean) bean).get(name, index);
366        }
367
368        // Retrieve the property descriptor for the specified property
369        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
370        if (descriptor == null) {
371            throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
372        }
373
374        // Call the indexed getter method if there is one
375        if (descriptor instanceof IndexedPropertyDescriptor) {
376            Method readMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedReadMethod();
377            readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
378            if (readMethod != null) {
379                try {
380                    return invokeMethod(readMethod, bean, Integer.valueOf(index));
381                } catch (final InvocationTargetException e) {
382                    if (e.getTargetException() instanceof IndexOutOfBoundsException) {
383                        throw (IndexOutOfBoundsException) e.getTargetException();
384                    }
385                    throw e;
386                }
387            }
388        }
389
390        // Otherwise, the underlying property must be an array
391        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
392        if (readMethod == null) {
393            throw new NoSuchMethodException("Property '" + name + "' has no " + "getter method on bean class '" + bean.getClass() + "'");
394        }
395
396        // Call the property getter and return the value
397        final Object value = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
398        if (!value.getClass().isArray()) {
399            if (!(value instanceof List)) {
400                throw new IllegalArgumentException("Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'");
401            }
402            // get the List's value
403            return ((List<?>) value).get(index);
404        }
405        // get the array's value
406        try {
407            return Array.get(value, index);
408        } catch (final ArrayIndexOutOfBoundsException e) {
409            throw new ArrayIndexOutOfBoundsException("Index: " + index + ", Size: " + Array.getLength(value) + " for property '" + name + "'");
410        }
411    }
412
413    /**
414     * Obtains the {@code BeanIntrospectionData} object describing the specified bean class. This object is looked up in the internal cache. If necessary,
415     * introspection is performed now on the affected bean class, and the results object is created.
416     *
417     * @param beanClass the bean class in question
418     * @return the {@code BeanIntrospectionData} object for this class
419     * @throws IllegalArgumentException if the bean class is <strong>null</strong>
420     */
421    private BeanIntrospectionData getIntrospectionData(final Class<?> beanClass) {
422        Objects.requireNonNull(beanClass, "beanClass");
423        // Look up any cached information for this bean class
424        BeanIntrospectionData data = descriptorsCache.get(beanClass);
425        if (data == null) {
426            data = fetchIntrospectionData(beanClass);
427            descriptorsCache.put(beanClass, data);
428        }
429        return data;
430    }
431
432    /**
433     * Gets the value of the specified mapped property of the specified bean, with no type conversions. The key of the required value must be included (in
434     * brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown.
435     *
436     * @param bean Bean whose property is to be extracted
437     * @param name {@code propertyname(key)} of the property value to be extracted
438     * @return the mapped property value
439     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
440     * @throws InvocationTargetException if the property accessor method throws an exception
441     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
442     */
443    public Object getMappedProperty(final Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
444        Objects.requireNonNull(bean, "bean");
445        Objects.requireNonNull(name, "name");
446        // Identify the key of the requested individual property
447        String key = null;
448        try {
449            key = resolver.getKey(name);
450        } catch (final IllegalArgumentException e) {
451            throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "' " + e.getMessage());
452        }
453        if (key == null) {
454            throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'");
455        }
456
457        // Isolate the name
458        name = resolver.getProperty(name);
459
460        // Request the specified indexed property value
461        return getMappedProperty(bean, name, key);
462    }
463
464    /**
465     * Gets the value of the specified mapped property of the specified bean, with no type conversions.
466     *
467     * @param bean Bean whose property is to be extracted
468     * @param name Mapped property name of the property value to be extracted
469     * @param key  Key of the property value to be extracted
470     * @return the mapped property value
471     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
472     * @throws InvocationTargetException if the property accessor method throws an exception
473     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
474     */
475    public Object getMappedProperty(final Object bean, final String name, final String key)
476            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
477        Objects.requireNonNull(bean, "bean");
478        Objects.requireNonNull(name, "name");
479        Objects.requireNonNull(key, "key");
480        // Handle DynaBean instances specially
481        if (bean instanceof DynaBean) {
482            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
483            if (descriptor == null) {
484                throw new NoSuchMethodException("Unknown property '" + name + "'+ on bean class '" + bean.getClass() + "'");
485            }
486            return ((DynaBean) bean).get(name, key);
487        }
488
489        Object result = null;
490
491        // Retrieve the property descriptor for the specified property
492        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
493        if (descriptor == null) {
494            throw new NoSuchMethodException("Unknown property '" + name + "'+ on bean class '" + bean.getClass() + "'");
495        }
496
497        if (descriptor instanceof MappedPropertyDescriptor) {
498            // Call the keyed getter method if there is one
499            Method readMethod = ((MappedPropertyDescriptor) descriptor).getMappedReadMethod();
500            readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
501            if (readMethod == null) {
502                throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'");
503            }
504            result = invokeMethod(readMethod, bean, key);
505        } else {
506            /* means that the result has to be retrieved from a map */
507            final Method readMethod = getReadMethod(bean.getClass(), descriptor);
508            if (readMethod == null) {
509                throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'");
510            }
511            final Object invokeResult = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
512            /* test and fetch from the map */
513            if (invokeResult instanceof Map) {
514                result = ((Map<?, ?>) invokeResult).get(key);
515            }
516        }
517        return result;
518    }
519
520    /**
521     * <p>
522     * Return the mapped property descriptors for this bean class.
523     * </p>
524     *
525     * <p>
526     * <strong>FIXME</strong> - Does not work with DynaBeans.
527     * </p>
528     *
529     * @param beanClass Bean class to be introspected
530     * @return the mapped property descriptors
531     */
532    Map<Class<?>, Map> getMappedPropertyDescriptors(final Class<?> beanClass) {
533        if (beanClass == null) {
534            return null;
535        }
536        // Look up any cached descriptors for this bean class
537        return mappedDescriptorsCache.get(beanClass);
538    }
539
540    /**
541     * <p>
542     * Return the mapped property descriptors for this bean.
543     * </p>
544     *
545     * <p>
546     * <strong>FIXME</strong> - Does not work with DynaBeans.
547     * </p>
548     *
549     * @param bean Bean to be introspected
550     * @return the mapped property descriptors
551     */
552    Map getMappedPropertyDescriptors(final Object bean) {
553        if (bean == null) {
554            return null;
555        }
556        return getMappedPropertyDescriptors(bean.getClass());
557    }
558
559    /**
560     * Gets the value of the (possibly nested) property of the specified name, for the specified bean, with no type conversions.
561     *
562     * @param bean Bean whose property is to be extracted
563     * @param name Possibly nested name of the property to be extracted
564     * @return the nested property value
565     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
566     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
567     * @throws NestedNullException       if a nested reference to a property returns null
568     * @throws InvocationTargetException if the property accessor method throws an exception
569     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
570     */
571    public Object getNestedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
572        Objects.requireNonNull(bean, "bean");
573        Objects.requireNonNull(name, "name");
574        // Resolve nested references
575        while (resolver.hasNested(name)) {
576            final String next = resolver.next(name);
577            Object nestedBean = null;
578            if (bean instanceof Map) {
579                nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next);
580            } else if (resolver.isMapped(next)) {
581                nestedBean = getMappedProperty(bean, next);
582            } else if (resolver.isIndexed(next)) {
583                nestedBean = getIndexedProperty(bean, next);
584            } else {
585                nestedBean = getSimpleProperty(bean, next);
586            }
587            if (nestedBean == null) {
588                throw new NestedNullException("Null property value for '" + name + "' on bean class '" + bean.getClass() + "'");
589            }
590            bean = nestedBean;
591            name = resolver.remove(name);
592        }
593
594        if (bean instanceof Map) {
595            bean = getPropertyOfMapBean((Map<?, ?>) bean, name);
596        } else if (resolver.isMapped(name)) {
597            bean = getMappedProperty(bean, name);
598        } else if (resolver.isIndexed(name)) {
599            bean = getIndexedProperty(bean, name);
600        } else {
601            bean = getSimpleProperty(bean, name);
602        }
603        return bean;
604    }
605
606    /**
607     * Gets the value of the specified property of the specified bean, no matter which property reference format is used, with no type conversions.
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 value
612     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
613     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
614     * @throws InvocationTargetException if the property accessor method throws an exception
615     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
616     */
617    public Object getProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
618        return getNestedProperty(bean, name);
619    }
620
621    /**
622     * <p>
623     * Retrieve the property descriptor for the specified property of the specified bean, or return {@code null} if there is no such descriptor. This method
624     * resolves indexed and nested property references in the same manner as other methods in this class, except that if the last (or only) name element is
625     * indexed, the descriptor for the last resolved property itself is returned.
626     * </p>
627     *
628     * <p>
629     * <strong>FIXME</strong> - Does not work with DynaBeans.
630     * </p>
631     *
632     * <p>
633     * Note that for Java 8 and above, this method no longer return IndexedPropertyDescriptor for {@link List}-typed properties, only for properties typed as
634     * native array. (BEANUTILS-492).
635     *
636     * @param bean Bean for which a property descriptor is requested
637     * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested
638     * @return the property descriptor
639     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
640     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
641     * @throws IllegalArgumentException  if a nested reference to a property returns null
642     * @throws InvocationTargetException if the property accessor method throws an exception
643     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
644     */
645    public PropertyDescriptor getPropertyDescriptor(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
646        Objects.requireNonNull(bean, "bean");
647        Objects.requireNonNull(name, "name");
648        // Resolve nested references
649        while (resolver.hasNested(name)) {
650            final String next = resolver.next(name);
651            final Object nestedBean = getProperty(bean, next);
652            if (nestedBean == null) {
653                throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'");
654            }
655            bean = nestedBean;
656            name = resolver.remove(name);
657        }
658
659        // Remove any subscript from the final name value
660        name = resolver.getProperty(name);
661
662        // Look up and return this property from our cache
663        // creating and adding it to the cache if not found.
664        if (name == null) {
665            return null;
666        }
667
668        final BeanIntrospectionData data = getIntrospectionData(bean.getClass());
669        PropertyDescriptor result = data.getDescriptor(name);
670        if (result != null) {
671            return result;
672        }
673
674        Map mappedDescriptors = getMappedPropertyDescriptors(bean);
675        if (mappedDescriptors == null) {
676            mappedDescriptors = new ConcurrentHashMap<Class<?>, Map>();
677            mappedDescriptorsCache.put(bean.getClass(), mappedDescriptors);
678        }
679        result = (PropertyDescriptor) mappedDescriptors.get(name);
680        if (result == null) {
681            // not found, try to create it
682            try {
683                result = new MappedPropertyDescriptor(name, bean.getClass());
684            } catch (final IntrospectionException ie) {
685                /*
686                 * Swallow IntrospectionException TODO: Why?
687                 */
688            }
689            if (result != null) {
690                mappedDescriptors.put(name, result);
691            }
692        }
693
694        return result;
695    }
696
697    /**
698     * <p>
699     * Retrieve the property descriptors for the specified class, introspecting and caching them the first time a particular bean class is encountered.
700     * </p>
701     *
702     * <p>
703     * <strong>FIXME</strong> - Does not work with DynaBeans.
704     * </p>
705     *
706     * @param beanClass Bean class for which property descriptors are requested
707     * @return the property descriptors
708     * @throws IllegalArgumentException if {@code beanClass} is null
709     */
710    public PropertyDescriptor[] getPropertyDescriptors(final Class<?> beanClass) {
711        return getIntrospectionData(beanClass).getDescriptors();
712    }
713
714    /**
715     * <p>
716     * Retrieve the property descriptors for the specified bean, introspecting and caching them the first time a particular bean class is encountered.
717     * </p>
718     *
719     * <p>
720     * <strong>FIXME</strong> - Does not work with DynaBeans.
721     * </p>
722     *
723     * @param bean Bean for which property descriptors are requested
724     * @return the property descriptors
725     * @throws IllegalArgumentException if {@code bean} is null
726     */
727    public PropertyDescriptor[] getPropertyDescriptors(final Object bean) {
728        Objects.requireNonNull(bean, "bean");
729        return getPropertyDescriptors(bean.getClass());
730    }
731
732    /**
733     * <p>
734     * Return the Java Class repesenting the property editor class that has been registered for this property (if any). This method follows the same name
735     * resolution rules used by {@code getPropertyDescriptor()}, so if the last element of a name reference is indexed, the property editor for the underlying
736     * property's class is returned.
737     * </p>
738     *
739     * <p>
740     * Note that {@code null} will be returned if there is no property, or if there is no registered property editor class. Because this return value is
741     * ambiguous, you should determine the existence of the property itself by other means.
742     * </p>
743     *
744     * <p>
745     * <strong>FIXME</strong> - Does not work with DynaBeans.
746     * </p>
747     *
748     * @param bean Bean for which a property descriptor is requested
749     * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested
750     * @return the property editor class
751     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
752     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
753     * @throws IllegalArgumentException  if a nested reference to a property returns null
754     * @throws InvocationTargetException if the property accessor method throws an exception
755     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
756     */
757    public Class<?> getPropertyEditorClass(final Object bean, final String name)
758            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
759        Objects.requireNonNull(bean, "bean");
760        Objects.requireNonNull(name, "name");
761        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
762        if (descriptor != null) {
763            return descriptor.getPropertyEditorClass();
764        }
765        return null;
766    }
767
768    /**
769     * This method is called by getNestedProperty and setNestedProperty to define what it means to get a property from an object which implements Map. See
770     * setPropertyOfMapBean for more information.
771     *
772     * @param bean         Map bean
773     * @param propertyName The property name
774     * @return the property value
775     * @throws IllegalArgumentException  when the propertyName is regarded as being invalid.
776     * @throws IllegalAccessException    just in case subclasses override this method to try to access real getter methods and find permission is denied.
777     * @throws InvocationTargetException just in case subclasses override this method to try to access real getter methods, and find it throws an exception when
778     *                                   invoked.
779     *
780     * @throws NoSuchMethodException     just in case subclasses override this method to try to access real getter methods, and want to fail if no simple method
781     *                                   is available.
782     * @since 1.8.0
783     */
784    protected Object getPropertyOfMapBean(final Map<?, ?> bean, String propertyName)
785            throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
786
787        if (resolver.isMapped(propertyName)) {
788            final String name = resolver.getProperty(propertyName);
789            if (name == null || name.isEmpty()) {
790                propertyName = resolver.getKey(propertyName);
791            }
792        }
793
794        if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) {
795            throw new IllegalArgumentException("Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName);
796        }
797
798        return bean.get(propertyName);
799    }
800
801    /**
802     * Gets the Java Class representing the property type of the specified property, or {@code null} if there is no such property for the specified bean. This
803     * method follows the same name resolution rules used by {@code getPropertyDescriptor()}, so if the last element of a name reference is indexed, the type of
804     * the property itself will be returned. If the last (or only) element has no property with the specified name, {@code null} is returned.
805     * <p>
806     * If the property is an indexed property (for example {@code String[]}), this method will return the type of the items within that array. Note that from
807     * Java 8 and newer, this method do not support such index types from items within an Collection, and will instead return the collection type (for example
808     * java.util.List) from the getter method.
809     * </p>
810     *
811     * @param bean Bean for which a property descriptor is requested
812     * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested
813     * @return The property type
814     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
815     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
816     * @throws IllegalArgumentException  if a nested reference to a property returns null
817     * @throws InvocationTargetException if the property accessor method throws an exception
818     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
819     */
820    public Class<?> getPropertyType(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
821        Objects.requireNonNull(bean, "bean");
822        Objects.requireNonNull(name, "name");
823        // Resolve nested references
824        while (resolver.hasNested(name)) {
825            final String next = resolver.next(name);
826            final Object nestedBean = getProperty(bean, next);
827            if (nestedBean == null) {
828                throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'");
829            }
830            bean = nestedBean;
831            name = resolver.remove(name);
832        }
833
834        // Remove any subscript from the final name value
835        name = resolver.getProperty(name);
836
837        // Special handling for DynaBeans
838        if (bean instanceof DynaBean) {
839            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
840            if (descriptor == null) {
841                return null;
842            }
843            final Class<?> type = descriptor.getType();
844            if (type == null) {
845                return null;
846            }
847            if (type.isArray()) {
848                return type.getComponentType();
849            }
850            return type;
851        }
852
853        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
854        if (descriptor == null) {
855            return null;
856        }
857        if (descriptor instanceof IndexedPropertyDescriptor) {
858            return ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType();
859        }
860        if (descriptor instanceof MappedPropertyDescriptor) {
861            return ((MappedPropertyDescriptor) descriptor).getMappedPropertyType();
862        }
863        return descriptor.getPropertyType();
864    }
865
866    /**
867     * <p>
868     * Return the property getter method for this property if accessible from given {@code clazz} (and if there is one at all); otherwise return {@code null}.
869     * </p>
870     *
871     * <p>
872     * <strong>FIXME</strong> - Does not work with DynaBeans.
873     * </p>
874     *
875     * <p>
876     * This fairly low-level method shouldn't be needed for most usecases. However, if you do have to implement something extra, you can improve consistency
877     * with the standard code (for example that of {@link #getProperty getProperty()}) by calling this method instead of using
878     * {@code descriptor.getReadMethod()} directly.
879     * </p>
880     *
881     * @param clazz      The class of the read method will be invoked on
882     * @param descriptor Property descriptor to return a getter for
883     * @return The read method
884     * @since 2.0.0
885     */
886    public Method getReadMethod(final Class<?> clazz, final PropertyDescriptor descriptor) {
887        return MethodUtils.getAccessibleMethod(clazz, descriptor.getReadMethod());
888    }
889
890    /**
891     * <p>
892     * Return an accessible property getter method for this property, if there is one; otherwise return {@code null}.
893     * </p>
894     *
895     * <p>
896     * <strong>FIXME</strong> - Does not work with DynaBeans.
897     * </p>
898     *
899     * @param descriptor Property descriptor to return a getter for
900     * @return The read method
901     */
902    public Method getReadMethod(final PropertyDescriptor descriptor) {
903        return MethodUtils.getAccessibleMethod(descriptor.getReadMethod());
904    }
905
906    /**
907     * Gets the configured {@link Resolver} implementation used by BeanUtils.
908     * <p>
909     * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression
910     * language</em> that BeanUtils recognizes.
911     * <p>
912     * {@link DefaultResolver} is the default implementation used.
913     *
914     * @return resolver The property expression resolver.
915     * @since 1.8.0
916     */
917    public Resolver getResolver() {
918        return resolver;
919    }
920
921    /**
922     * Gets the value of the specified simple property of the specified bean, with no type conversions.
923     *
924     * @param bean Bean whose property is to be extracted
925     * @param name Name of the property to be extracted
926     * @return The property value
927     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
928     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
929     * @throws IllegalArgumentException  if the property name is nested or indexed
930     * @throws InvocationTargetException if the property accessor method throws an exception
931     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
932     */
933    public Object getSimpleProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
934        Objects.requireNonNull(bean, "bean");
935        Objects.requireNonNull(name, "name");
936        // Validate the syntax of the property name
937        if (resolver.hasNested(name)) {
938            throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
939        }
940        if (resolver.isIndexed(name)) {
941            throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
942        }
943        if (resolver.isMapped(name)) {
944            throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'");
945        }
946
947        // Handle DynaBean instances specially
948        if (bean instanceof DynaBean) {
949            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
950            if (descriptor == null) {
951                throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'");
952            }
953            return ((DynaBean) bean).get(name);
954        }
955
956        // Retrieve the property getter method for the specified property
957        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
958        if (descriptor == null) {
959            throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + bean.getClass() + "'");
960        }
961        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
962        if (readMethod == null) {
963            throw new NoSuchMethodException("Property '" + name + "' has no getter method in class '" + bean.getClass() + "'");
964        }
965
966        // Call the property getter and return the value
967        return invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
968    }
969
970    /**
971     * <p>
972     * Return the property setter method for this property if accessible from given {@code clazz} (and if there is one at all); otherwise return {@code null}.
973     * </p>
974     *
975     * <p>
976     * <strong>FIXME</strong> - Does not work with DynaBeans.
977     * </p>
978     *
979     * <p>
980     * This fairly low-level method shouldn't be needed for most usecases. However, if you do have to implement something extra, you can improve consistency
981     * with the standard code (for example that of {@link #setProperty setProperty()}) by calling this method instead of using
982     * {@code descriptor.getWriteMethod()} directly.
983     * </p>
984     *
985     * @param clazz      The class of the read method will be invoked on
986     * @param descriptor Property descriptor to return a setter for
987     * @return The write method
988     * @since 1.9.1
989     */
990    public Method getWriteMethod(final Class<?> clazz, final PropertyDescriptor descriptor) {
991        final BeanIntrospectionData data = getIntrospectionData(clazz);
992        return MethodUtils.getAccessibleMethod(clazz, data.getWriteMethod(clazz, descriptor));
993    }
994
995    /**
996     * <p>
997     * Return an accessible property setter method for this property, if there is one; otherwise return {@code null}.
998     * </p>
999     *
1000     * <p>
1001     * <em>Note:</em> This method does not work correctly with custom bean introspection under certain circumstances. It may return {@code null} even if a write
1002     * method is defined for the property in question. Use {@link #getWriteMethod(Class, PropertyDescriptor)} to be sure that the correct result is returned.
1003     * </p>
1004     * <p>
1005     * <strong>FIXME</strong> - Does not work with DynaBeans.
1006     * </p>
1007     *
1008     * @param descriptor Property descriptor to return a setter for
1009     * @return The write method
1010     */
1011    public Method getWriteMethod(final PropertyDescriptor descriptor) {
1012        return MethodUtils.getAccessibleMethod(descriptor.getWriteMethod());
1013    }
1014
1015    /**
1016     * Delegates to {@link Method#invoke(Object, Object...)} and handles some unchecked exceptions.
1017     *
1018     * @see Method#invoke(Object, Object...)
1019     */
1020    private Object invokeMethod(final Method method, final Object bean, final Object... values) throws IllegalAccessException, InvocationTargetException {
1021        Objects.requireNonNull(bean, "bean");
1022        try {
1023            return method.invoke(bean, values);
1024        } catch (final NullPointerException | IllegalArgumentException cause) {
1025            // JDK 1.3 and JDK 1.4 throw NullPointerException if an argument is
1026            // null for a primitive value (JDK 1.5+ throw IllegalArgumentException)
1027            final StringBuilder valueString = new StringBuilder();
1028            if (values != null) {
1029                for (int i = 0; i < values.length; i++) {
1030                    if (i > 0) {
1031                        valueString.append(", ");
1032                    }
1033                    if (values[i] == null) {
1034                        valueString.append("<null>");
1035                    } else {
1036                        valueString.append(values[i].getClass().getName());
1037                    }
1038                }
1039            }
1040            final StringBuilder expectedString = new StringBuilder();
1041            final Class<?>[] parTypes = method.getParameterTypes();
1042            if (parTypes != null) {
1043                for (int i = 0; i < parTypes.length; i++) {
1044                    if (i > 0) {
1045                        expectedString.append(", ");
1046                    }
1047                    expectedString.append(parTypes[i].getName());
1048                }
1049            }
1050            throw new IllegalArgumentException("Cannot invoke " + method.getDeclaringClass().getName() + "." + method.getName() + " on bean class '"
1051                    + bean.getClass() + "' - " + cause.getMessage()
1052                    // as per https://issues.apache.org/jira/browse/BEANUTILS-224
1053                    + " - had objects of type \"" + valueString + "\" but expected signature \"" + expectedString + "\"", cause);
1054        }
1055    }
1056
1057    /**
1058     * Return {@code true} if the specified property name identifies a readable property on the specified bean; otherwise, return {@code false}.
1059     *
1060     * @param bean Bean to be examined (may be a {@link DynaBean}
1061     * @param name Property name to be evaluated
1062     * @return {@code true} if the property is readable, otherwise {@code false}
1063     * @throws IllegalArgumentException if {@code bean} or {@code name</code> is <code>null}
1064     * @since 1.6
1065     */
1066    public boolean isReadable(Object bean, String name) {
1067        // Validate method parameters
1068        Objects.requireNonNull(bean, "bean");
1069        Objects.requireNonNull(name, "name");
1070        // Resolve nested references
1071        while (resolver.hasNested(name)) {
1072            final String next = resolver.next(name);
1073            Object nestedBean = null;
1074            try {
1075                nestedBean = getProperty(bean, next);
1076            } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
1077                return false;
1078            }
1079            if (nestedBean == null) {
1080                throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'");
1081            }
1082            bean = nestedBean;
1083            name = resolver.remove(name);
1084        }
1085
1086        // Remove any subscript from the final name value
1087        name = resolver.getProperty(name);
1088
1089        // Treat WrapDynaBean as special case - may be a write-only property
1090        // (see Jira issue# BEANUTILS-61)
1091        if (bean instanceof WrapDynaBean) {
1092            bean = ((WrapDynaBean) bean).getInstance();
1093        }
1094
1095        // Return the requested result
1096        if (bean instanceof DynaBean) {
1097            // All DynaBean properties are readable
1098            return ((DynaBean) bean).getDynaClass().getDynaProperty(name) != null;
1099        }
1100        try {
1101            final PropertyDescriptor desc = getPropertyDescriptor(bean, name);
1102            if (desc != null) {
1103                Method readMethod = getReadMethod(bean.getClass(), desc);
1104                if (readMethod == null) {
1105                    if (desc instanceof IndexedPropertyDescriptor) {
1106                        readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod();
1107                    } else if (desc instanceof MappedPropertyDescriptor) {
1108                        readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod();
1109                    }
1110                    readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod);
1111                }
1112                return readMethod != null;
1113            }
1114            return false;
1115        } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
1116            return false;
1117        }
1118    }
1119
1120    /**
1121     * Return {@code true} if the specified property name identifies a writable property on the specified bean; otherwise, return {@code false}.
1122     *
1123     * @param bean Bean to be examined (may be a {@link DynaBean}
1124     * @param name Property name to be evaluated
1125     * @return {@code true} if the property is writable, otherwise {@code false}
1126     * @throws IllegalArgumentException if {@code bean} or {@code name</code> is <code>null}
1127     * @since 1.6
1128     */
1129    public boolean isWriteable(Object bean, String name) {
1130        // Validate method parameters
1131        Objects.requireNonNull(bean, "bean");
1132        Objects.requireNonNull(name, "name");
1133        // Resolve nested references
1134        while (resolver.hasNested(name)) {
1135            final String next = resolver.next(name);
1136            Object nestedBean = null;
1137            try {
1138                nestedBean = getProperty(bean, next);
1139            } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
1140                return false;
1141            }
1142            if (nestedBean == null) {
1143                throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'");
1144            }
1145            bean = nestedBean;
1146            name = resolver.remove(name);
1147        }
1148
1149        // Remove any subscript from the final name value
1150        name = resolver.getProperty(name);
1151
1152        // Treat WrapDynaBean as special case - may be a read-only property
1153        // (see Jira issue# BEANUTILS-61)
1154        if (bean instanceof WrapDynaBean) {
1155            bean = ((WrapDynaBean) bean).getInstance();
1156        }
1157
1158        // Return the requested result
1159        if (bean instanceof DynaBean) {
1160            // All DynaBean properties are writable
1161            return ((DynaBean) bean).getDynaClass().getDynaProperty(name) != null;
1162        }
1163        try {
1164            final PropertyDescriptor desc = getPropertyDescriptor(bean, name);
1165            if (desc != null) {
1166                Method writeMethod = getWriteMethod(bean.getClass(), desc);
1167                if (writeMethod == null) {
1168                    if (desc instanceof IndexedPropertyDescriptor) {
1169                        writeMethod = ((IndexedPropertyDescriptor) desc).getIndexedWriteMethod();
1170                    } else if (desc instanceof MappedPropertyDescriptor) {
1171                        writeMethod = ((MappedPropertyDescriptor) desc).getMappedWriteMethod();
1172                    }
1173                    writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
1174                }
1175                return writeMethod != null;
1176            }
1177            return false;
1178        } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
1179            return false;
1180        }
1181    }
1182
1183    /**
1184     * Removes the specified {@code BeanIntrospector}.
1185     *
1186     * @param introspector the {@code BeanIntrospector} to be removed
1187     * @return <strong>true</strong> if the {@code BeanIntrospector} existed and could be removed, <strong>false</strong> otherwise
1188     * @since 1.9
1189     */
1190    public boolean removeBeanIntrospector(final BeanIntrospector introspector) {
1191        return introspectors.remove(introspector);
1192    }
1193
1194    /**
1195     * Resets the {@link BeanIntrospector} objects registered at this instance. After this method was called, only the default {@code BeanIntrospector} is
1196     * registered.
1197     *
1198     * @since 1.9
1199     */
1200    public final void resetBeanIntrospectors() {
1201        introspectors.clear();
1202        introspectors.add(DefaultBeanIntrospector.INSTANCE);
1203        introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS);
1204    }
1205
1206    /**
1207     * Sets the value of the specified indexed property of the specified bean, with no type conversions. In addition to supporting the JavaBeans specification,
1208     * this method has been extended to support {@code List} objects as well.
1209     *
1210     * @param bean  Bean whose property is to be set
1211     * @param name  Simple property name of the property value to be set
1212     * @param index Index of the property value to be set
1213     * @param value Value to which the indexed property element is to be set
1214     * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property
1215     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1216     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1217     * @throws InvocationTargetException if the property accessor method throws an exception
1218     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1219     */
1220    public void setIndexedProperty(final Object bean, final String name, final int index, final Object value)
1221            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1222        Objects.requireNonNull(bean, "bean");
1223        if (name == null || name.isEmpty()) {
1224            if (bean.getClass().isArray()) {
1225                Array.set(bean, index, value);
1226                return;
1227            }
1228            if (bean instanceof List) {
1229                final List<Object> list = toObjectList(bean);
1230                list.set(index, value);
1231                return;
1232            }
1233        }
1234        Objects.requireNonNull(name, "name");
1235        // Handle DynaBean instances specially
1236        if (bean instanceof DynaBean) {
1237            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1238            if (descriptor == null) {
1239                throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1240            }
1241            ((DynaBean) bean).set(name, index, value);
1242            return;
1243        }
1244
1245        // Retrieve the property descriptor for the specified property
1246        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1247        if (descriptor == null) {
1248            throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1249        }
1250
1251        // Call the indexed setter method if there is one
1252        if (descriptor instanceof IndexedPropertyDescriptor) {
1253            Method writeMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod();
1254            writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
1255            if (writeMethod != null) {
1256                try {
1257                    if (LOG.isTraceEnabled()) {
1258                        final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1259                        LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with index=" + index + ", value=" + value + " (class "
1260                                + valueClassName + ")");
1261                    }
1262                    invokeMethod(writeMethod, bean, Integer.valueOf(index), value);
1263                } catch (final InvocationTargetException e) {
1264                    if (e.getTargetException() instanceof IndexOutOfBoundsException) {
1265                        throw (IndexOutOfBoundsException) e.getTargetException();
1266                    }
1267                    throw e;
1268                }
1269                return;
1270            }
1271        }
1272
1273        // Otherwise, the underlying property must be an array or a list
1274        final Method readMethod = getReadMethod(bean.getClass(), descriptor);
1275        if (readMethod == null) {
1276            throw new NoSuchMethodException("Property '" + name + "' has no getter method on bean class '" + bean.getClass() + "'");
1277        }
1278
1279        // Call the property getter to get the array or list
1280        final Object array = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
1281        if (!array.getClass().isArray()) {
1282            if (!(array instanceof List)) {
1283                throw new IllegalArgumentException("Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'");
1284            }
1285            // Modify the specified value in the List
1286            final List<Object> list = toObjectList(array);
1287            list.set(index, value);
1288        } else {
1289            // Modify the specified value in the array
1290            Array.set(array, index, value);
1291        }
1292    }
1293
1294    /**
1295     * Sets the value of the specified indexed property of the specified bean, with no type conversions. The zero-relative index of the required value must be
1296     * included (in square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. In addition to supporting the
1297     * JavaBeans specification, this method has been extended to support {@code List} objects as well.
1298     *
1299     * @param bean  Bean whose property is to be modified
1300     * @param name  {@code propertyname[index]} of the property value to be modified
1301     * @param value Value to which the specified property element should be set
1302     * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property
1303     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1304     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1305     * @throws InvocationTargetException if the property accessor method throws an exception
1306     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1307     */
1308    public void setIndexedProperty(final Object bean, String name, final Object value)
1309            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1310        Objects.requireNonNull(bean, "bean");
1311        Objects.requireNonNull(name, "name");
1312        // Identify the index of the requested individual property
1313        int index = -1;
1314        try {
1315            index = resolver.getIndex(name);
1316        } catch (final IllegalArgumentException e) {
1317            throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'");
1318        }
1319        if (index < 0) {
1320            throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'");
1321        }
1322
1323        // Isolate the name
1324        name = resolver.getProperty(name);
1325
1326        // Set the specified indexed property value
1327        setIndexedProperty(bean, name, index, value);
1328    }
1329
1330    /**
1331     * Sets the value of the specified mapped property of the specified bean, with no type conversions. The key of the value to set must be included (in
1332     * brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown.
1333     *
1334     * @param bean  Bean whose property is to be set
1335     * @param name  {@code propertyname(key)} of the property value to be set
1336     * @param value The property value to be set
1337     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1338     * @throws InvocationTargetException if the property accessor method throws an exception
1339     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1340     */
1341    public void setMappedProperty(final Object bean, String name, final Object value)
1342            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1343        Objects.requireNonNull(bean, "bean");
1344        Objects.requireNonNull(name, "name");
1345
1346        // Identify the key of the requested individual property
1347        String key = null;
1348        try {
1349            key = resolver.getKey(name);
1350        } catch (final IllegalArgumentException e) {
1351            throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'");
1352        }
1353        if (key == null) {
1354            throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'");
1355        }
1356
1357        // Isolate the name
1358        name = resolver.getProperty(name);
1359
1360        // Request the specified indexed property value
1361        setMappedProperty(bean, name, key, value);
1362    }
1363
1364    /**
1365     * Sets the value of the specified mapped property of the specified bean, with no type conversions.
1366     *
1367     * @param bean  Bean whose property is to be set
1368     * @param name  Mapped property name of the property value to be set
1369     * @param key   Key of the property value to be set
1370     * @param value The property value to be set
1371     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1372     * @throws InvocationTargetException if the property accessor method throws an exception
1373     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1374     */
1375    public void setMappedProperty(final Object bean, final String name, final String key, final Object value)
1376            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1377        Objects.requireNonNull(bean, "bean");
1378        Objects.requireNonNull(name, "name");
1379        Objects.requireNonNull(key, "key");
1380        // Handle DynaBean instances specially
1381        if (bean instanceof DynaBean) {
1382            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1383            if (descriptor == null) {
1384                throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1385            }
1386            ((DynaBean) bean).set(name, key, value);
1387            return;
1388        }
1389
1390        // Retrieve the property descriptor for the specified property
1391        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1392        if (descriptor == null) {
1393            throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1394        }
1395
1396        if (descriptor instanceof MappedPropertyDescriptor) {
1397            // Call the keyed setter method if there is one
1398            Method mappedWriteMethod = ((MappedPropertyDescriptor) descriptor).getMappedWriteMethod();
1399            mappedWriteMethod = MethodUtils.getAccessibleMethod(bean.getClass(), mappedWriteMethod);
1400            if (mappedWriteMethod == null) {
1401                throw new NoSuchMethodException("Property '" + name + "' has no mapped setter method" + "on bean class '" + bean.getClass() + "'");
1402            }
1403            if (LOG.isTraceEnabled()) {
1404                final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1405                LOG.trace("setSimpleProperty: Invoking method " + mappedWriteMethod + " with key=" + key + ", value=" + value + " (class " + valueClassName
1406                        + ")");
1407            }
1408            invokeMethod(mappedWriteMethod, bean, key, value);
1409        } else {
1410            /* means that the result has to be retrieved from a map */
1411            final Method readMethod = getReadMethod(bean.getClass(), descriptor);
1412            if (readMethod == null) {
1413                throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'");
1414            }
1415            final Object invokeResult = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
1416            /* test and fetch from the map */
1417            if (invokeResult instanceof Map) {
1418                toPropertyMap(invokeResult).put(key, value);
1419            }
1420        }
1421    }
1422
1423    /**
1424     * Sets the value of the (possibly nested) property of the specified name, for the specified bean, with no type conversions.
1425     * <p>
1426     * Example values for parameter "name" are:
1427     * <ul>
1428     * <li>"a" -- sets the value of property a of the specified bean</li>
1429     * <li>"a.b" -- gets the value of property a of the specified bean, then on that object sets the value of property b.</li>
1430     * <li>"a(key)" -- sets a value of mapped-property a on the specified bean. This effectively means bean.setA("key").</li>
1431     * <li>"a[3]" -- sets a value of indexed-property a on the specified bean. This effectively means bean.setA(3).</li>
1432     * </ul>
1433     *
1434     * @param bean  Bean whose property is to be modified
1435     * @param name  Possibly nested name of the property to be modified
1436     * @param value Value to which the property is to be set
1437     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1438     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1439     * @throws IllegalArgumentException  if a nested reference to a property returns null
1440     * @throws InvocationTargetException if the property accessor method throws an exception
1441     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1442     */
1443    public void setNestedProperty(Object bean, String name, final Object value)
1444            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1445        Objects.requireNonNull(bean, "bean");
1446        Objects.requireNonNull(name, "name");
1447        // Resolve nested references
1448        while (resolver.hasNested(name)) {
1449            final String next = resolver.next(name);
1450            Object nestedBean = null;
1451            if (bean instanceof Map) {
1452                nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next);
1453            } else if (resolver.isMapped(next)) {
1454                nestedBean = getMappedProperty(bean, next);
1455            } else if (resolver.isIndexed(next)) {
1456                nestedBean = getIndexedProperty(bean, next);
1457            } else {
1458                nestedBean = getSimpleProperty(bean, next);
1459            }
1460            if (nestedBean == null) {
1461                throw new NestedNullException("Null property value for '" + name + "' on bean class '" + bean.getClass() + "'");
1462            }
1463            bean = nestedBean;
1464            name = resolver.remove(name);
1465        }
1466
1467        if (bean instanceof Map) {
1468            setPropertyOfMapBean(toPropertyMap(bean), name, value);
1469        } else if (resolver.isMapped(name)) {
1470            setMappedProperty(bean, name, value);
1471        } else if (resolver.isIndexed(name)) {
1472            setIndexedProperty(bean, name, value);
1473        } else {
1474            setSimpleProperty(bean, name, value);
1475        }
1476    }
1477
1478    /**
1479     * Sets the value of the specified property of the specified bean, no matter which property reference format is used, with no type conversions.
1480     *
1481     * @param bean  Bean whose property is to be modified
1482     * @param name  Possibly indexed and/or nested name of the property to be modified
1483     * @param value Value to which this property is to be set
1484     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1485     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1486     * @throws InvocationTargetException if the property accessor method throws an exception
1487     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1488     */
1489    public void setProperty(final Object bean, final String name, final Object value)
1490            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1491        setNestedProperty(bean, name, value);
1492    }
1493
1494    /**
1495     * This method is called by method setNestedProperty when the current bean is found to be a Map object, and defines how to deal with setting a property on a
1496     * Map.
1497     * <p>
1498     * The standard implementation here is to:
1499     * <ul>
1500     * <li>call bean.set(propertyName) for all propertyName values.</li>
1501     * <li>throw an IllegalArgumentException if the property specifier contains MAPPED_DELIM or INDEXED_DELIM, as Map entries are essentially simple properties;
1502     * mapping and indexing operations do not make sense when accessing a map (even thought the returned object may be a Map or an Array).</li>
1503     * </ul>
1504     * <p>
1505     * The default behavior of BeanUtils 1.7.1 or later is for assigning to "a.b" to mean a.put(b, obj) always. However the behavior of BeanUtils version 1.6.0,
1506     * 1.6.1, 1.7.0 was for "a.b" to mean a.setB(obj) if such a method existed, and a.put(b, obj) otherwise. In version 1.5 it meant a.put(b, obj) always (ie
1507     * the same as the behavior in the current version). In versions prior to 1.5 it meant a.setB(obj) always. [yes, this is all <em>very</em> unfortunate]
1508     * <p>
1509     * Users who would like to customize the meaning of "a.b" in method setNestedProperty when a is a Map can create a custom subclass of this class and
1510     * override this method to implement the behavior of their choice, such as restoring the pre-1.4 behavior of this class if they wish. When overriding this
1511     * method, do not forget to deal with MAPPED_DELIM and INDEXED_DELIM characters in the propertyName.
1512     * <p>
1513     * Note, however, that the recommended solution for objects that implement Map but want their simple properties to come first is for <em>those</em> objects
1514     * to override their get/put methods to implement that behavior, and <em>not</em> to solve the problem by modifying the default behavior of the
1515     * PropertyUtilsBean class by overriding this method.
1516     *
1517     * @param bean         Map bean
1518     * @param propertyName The property name
1519     * @param value        the property value
1520     * @throws IllegalArgumentException  when the propertyName is regarded as being invalid.
1521     * @throws IllegalAccessException    just in case subclasses override this method to try to access real setter methods and find permission is denied.
1522     * @throws InvocationTargetException just in case subclasses override this method to try to access real setter methods, and find it throws an exception when
1523     *                                   invoked.
1524     *
1525     * @throws NoSuchMethodException     just in case subclasses override this method to try to access real setter methods, and want to fail if no simple method
1526     *                                   is available.
1527     * @since 1.8.0
1528     */
1529    protected void setPropertyOfMapBean(final Map<String, Object> bean, String propertyName, final Object value)
1530            throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1531        if (resolver.isMapped(propertyName)) {
1532            final String name = resolver.getProperty(propertyName);
1533            if (name == null || name.isEmpty()) {
1534                propertyName = resolver.getKey(propertyName);
1535            }
1536        }
1537
1538        if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) {
1539            throw new IllegalArgumentException("Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName);
1540        }
1541
1542        bean.put(propertyName, value);
1543    }
1544
1545    /**
1546     * Configure the {@link Resolver} implementation used by BeanUtils.
1547     * <p>
1548     * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression
1549     * language</em> that BeanUtils recognizes.
1550     * <p>
1551     * {@link DefaultResolver} is the default implementation used.
1552     *
1553     * @param resolver The property expression resolver.
1554     * @since 1.8.0
1555     */
1556    public void setResolver(final Resolver resolver) {
1557        if (resolver == null) {
1558            this.resolver = new DefaultResolver();
1559        } else {
1560            this.resolver = resolver;
1561        }
1562    }
1563
1564    /**
1565     * Sets the value of the specified simple property of the specified bean, with no type conversions.
1566     *
1567     * @param bean  Bean whose property is to be modified
1568     * @param name  Name of the property to be modified
1569     * @param value Value to which the property should be set
1570     * @throws IllegalAccessException    if the caller does not have access to the property accessor method
1571     * @throws IllegalArgumentException  if {@code bean} or {@code name} is null
1572     * @throws IllegalArgumentException  if the property name is nested or indexed
1573     * @throws InvocationTargetException if the property accessor method throws an exception
1574     * @throws NoSuchMethodException     if an accessor method for this property cannot be found
1575     */
1576    public void setSimpleProperty(final Object bean, final String name, final Object value)
1577            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1578        Objects.requireNonNull(bean, "bean");
1579        Objects.requireNonNull(name, "name");
1580        final Class<?> beanClass = bean.getClass();
1581        // Validate the syntax of the property name
1582        if (resolver.hasNested(name)) {
1583            throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1584        }
1585        if (resolver.isIndexed(name)) {
1586            throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1587        }
1588        if (resolver.isMapped(name)) {
1589            throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1590        }
1591
1592        // Handle DynaBean instances specially
1593        if (bean instanceof DynaBean) {
1594            final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1595            if (descriptor == null) {
1596                throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'");
1597            }
1598            ((DynaBean) bean).set(name, value);
1599            return;
1600        }
1601
1602        // Retrieve the property setter method for the specified property
1603        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1604        if (descriptor == null) {
1605            throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + beanClass + "'");
1606        }
1607        final Method writeMethod = getWriteMethod(beanClass, descriptor);
1608        if (writeMethod == null) {
1609            throw new NoSuchMethodException("Property '" + name + "' has no setter method in class '" + beanClass + "'");
1610        }
1611
1612        // Call the property setter method
1613        if (LOG.isTraceEnabled()) {
1614            final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1615            LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with value " + value + " (class " + valueClassName + ")");
1616        }
1617        invokeMethod(writeMethod, bean, value);
1618    }
1619}