View Javadoc
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  
18  package org.apache.commons.beanutils2;
19  
20  import java.beans.IndexedPropertyDescriptor;
21  import java.beans.IntrospectionException;
22  import java.beans.Introspector;
23  import java.beans.PropertyDescriptor;
24  import java.lang.reflect.Array;
25  import java.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Method;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  import java.util.concurrent.ConcurrentHashMap;
32  import java.util.concurrent.CopyOnWriteArrayList;
33  
34  import org.apache.commons.beanutils2.expression.DefaultResolver;
35  import org.apache.commons.beanutils2.expression.Resolver;
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  
39  /**
40   * Utility methods for using Java Reflection APIs to facilitate generic property getter and setter operations on Java objects. Much of this code was originally
41   * included in {@code BeanUtils}, but has been separated because of the volume of code involved.
42   * <p>
43   * In general, the objects that are examined and modified using these methods are expected to conform to the property getter and setter method naming
44   * conventions described in the JavaBeans Specification (Version 1.0.1). No data type conversions are performed, and there are no usage of any
45   * {@code PropertyEditor} classes that have been registered, although a convenient way to access the registered classes themselves is included.
46   * <p>
47   * 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
48   * 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
49   * configured {@link Resolver} implementation:
50   * <ul>
51   * <li><strong>Simple ({@code name})</strong> - The specified {@code name} identifies an individual property of a particular JavaBean. The name of the actual
52   * getter or setter method to be used is determined using standard JavaBeans introspection, so that (unless overridden by a {@code BeanInfo} class, a property
53   * named "xyz" will have a getter method named {@code getXyz()} or (for boolean properties only) {@code isXyz()}, and a setter method named
54   * {@code setXyz()}.</li>
55   * <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
56   * 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
57   * property value that is ultimately retrieved or modified is the one identified by the last name element.</li>
58   * <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
59   * property getter and setter methods. The appropriate (zero-relative) entry in the array is selected. {@code List} objects are now also supported for
60   * read/write. You simply need to define a getter that returns the {@code List}</li>
61   * <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
62   * {@link String}.</li>
63   * <li><strong>Combined ({@code name1.name2[index].name3(key)})</strong> - Combining mapped, nested, and indexed references is also supported.</li>
64   * </ul>
65   *
66   * @see Resolver
67   * @see PropertyUtils
68   * @since 1.7
69   */
70  public class PropertyUtilsBean {
71  
72      /** Log instance */
73      private static final Log LOG = LogFactory.getLog(PropertyUtilsBean.class);
74  
75      /**
76       * Gets the PropertyUtils bean instance.
77       *
78       * @return The PropertyUtils bean instance
79       */
80      protected static PropertyUtilsBean getInstance() {
81          return BeanUtilsBean.getInstance().getPropertyUtils();
82      }
83  
84      /**
85       * 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
86       * of objects.
87       *
88       * @param obj the object to be converted
89       * @return the resulting list of objects
90       */
91      @SuppressWarnings("unchecked")
92      private static List<Object> toObjectList(final Object obj) {
93          // indexed properties are stored in lists of objects
94          return (List<Object>) obj;
95      }
96  
97      /**
98       * 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
99       * 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 }