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 * https://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<String, Object>.
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 introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_DECLARING_CLASS);
1205 }
1206
1207 /**
1208 * Sets the value of the specified indexed property of the specified bean, with no type conversions. In addition to supporting the JavaBeans specification,
1209 * this method has been extended to support {@code List} objects as well.
1210 *
1211 * @param bean Bean whose property is to be set
1212 * @param name Simple property name of the property value to be set
1213 * @param index Index of the property value to be set
1214 * @param value Value to which the indexed property element is to be set
1215 * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property
1216 * @throws IllegalAccessException if the caller does not have access to the property accessor method
1217 * @throws IllegalArgumentException if {@code bean} or {@code name} is null
1218 * @throws InvocationTargetException if the property accessor method throws an exception
1219 * @throws NoSuchMethodException if an accessor method for this property cannot be found
1220 */
1221 public void setIndexedProperty(final Object bean, final String name, final int index, final Object value)
1222 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1223 Objects.requireNonNull(bean, "bean");
1224 if (name == null || name.isEmpty()) {
1225 if (bean.getClass().isArray()) {
1226 Array.set(bean, index, value);
1227 return;
1228 }
1229 if (bean instanceof List) {
1230 final List<Object> list = toObjectList(bean);
1231 list.set(index, value);
1232 return;
1233 }
1234 }
1235 Objects.requireNonNull(name, "name");
1236 // Handle DynaBean instances specially
1237 if (bean instanceof DynaBean) {
1238 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1239 if (descriptor == null) {
1240 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1241 }
1242 ((DynaBean) bean).set(name, index, value);
1243 return;
1244 }
1245
1246 // Retrieve the property descriptor for the specified property
1247 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1248 if (descriptor == null) {
1249 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1250 }
1251
1252 // Call the indexed setter method if there is one
1253 if (descriptor instanceof IndexedPropertyDescriptor) {
1254 Method writeMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod();
1255 writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod);
1256 if (writeMethod != null) {
1257 try {
1258 if (LOG.isTraceEnabled()) {
1259 final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1260 LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with index=" + index + ", value=" + value + " (class "
1261 + valueClassName + ")");
1262 }
1263 invokeMethod(writeMethod, bean, Integer.valueOf(index), value);
1264 } catch (final InvocationTargetException e) {
1265 if (e.getTargetException() instanceof IndexOutOfBoundsException) {
1266 throw (IndexOutOfBoundsException) e.getTargetException();
1267 }
1268 throw e;
1269 }
1270 return;
1271 }
1272 }
1273
1274 // Otherwise, the underlying property must be an array or a list
1275 final Method readMethod = getReadMethod(bean.getClass(), descriptor);
1276 if (readMethod == null) {
1277 throw new NoSuchMethodException("Property '" + name + "' has no getter method on bean class '" + bean.getClass() + "'");
1278 }
1279
1280 // Call the property getter to get the array or list
1281 final Object array = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
1282 if (!array.getClass().isArray()) {
1283 if (!(array instanceof List)) {
1284 throw new IllegalArgumentException("Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'");
1285 }
1286 // Modify the specified value in the List
1287 final List<Object> list = toObjectList(array);
1288 list.set(index, value);
1289 } else {
1290 // Modify the specified value in the array
1291 Array.set(array, index, value);
1292 }
1293 }
1294
1295 /**
1296 * 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
1297 * included (in square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. In addition to supporting the
1298 * JavaBeans specification, this method has been extended to support {@code List} objects as well.
1299 *
1300 * @param bean Bean whose property is to be modified
1301 * @param name {@code propertyname[index]} of the property value to be modified
1302 * @param value Value to which the specified property element should be set
1303 * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property
1304 * @throws IllegalAccessException if the caller does not have access to the property accessor method
1305 * @throws IllegalArgumentException if {@code bean} or {@code name} is null
1306 * @throws InvocationTargetException if the property accessor method throws an exception
1307 * @throws NoSuchMethodException if an accessor method for this property cannot be found
1308 */
1309 public void setIndexedProperty(final Object bean, String name, final Object value)
1310 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1311 Objects.requireNonNull(bean, "bean");
1312 Objects.requireNonNull(name, "name");
1313 // Identify the index of the requested individual property
1314 int index = -1;
1315 try {
1316 index = resolver.getIndex(name);
1317 } catch (final IllegalArgumentException e) {
1318 throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'");
1319 }
1320 if (index < 0) {
1321 throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'");
1322 }
1323
1324 // Isolate the name
1325 name = resolver.getProperty(name);
1326
1327 // Set the specified indexed property value
1328 setIndexedProperty(bean, name, index, value);
1329 }
1330
1331 /**
1332 * 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
1333 * brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown.
1334 *
1335 * @param bean Bean whose property is to be set
1336 * @param name {@code propertyname(key)} of the property value to be set
1337 * @param value The property value to be set
1338 * @throws IllegalAccessException if the caller does not have access to the property accessor method
1339 * @throws InvocationTargetException if the property accessor method throws an exception
1340 * @throws NoSuchMethodException if an accessor method for this property cannot be found
1341 */
1342 public void setMappedProperty(final Object bean, String name, final Object value)
1343 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1344 Objects.requireNonNull(bean, "bean");
1345 Objects.requireNonNull(name, "name");
1346
1347 // Identify the key of the requested individual property
1348 String key = null;
1349 try {
1350 key = resolver.getKey(name);
1351 } catch (final IllegalArgumentException e) {
1352 throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'");
1353 }
1354 if (key == null) {
1355 throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'");
1356 }
1357
1358 // Isolate the name
1359 name = resolver.getProperty(name);
1360
1361 // Request the specified indexed property value
1362 setMappedProperty(bean, name, key, value);
1363 }
1364
1365 /**
1366 * Sets the value of the specified mapped property of the specified bean, with no type conversions.
1367 *
1368 * @param bean Bean whose property is to be set
1369 * @param name Mapped property name of the property value to be set
1370 * @param key Key of the property value to be set
1371 * @param value The property value to be set
1372 * @throws IllegalAccessException if the caller does not have access to the property accessor method
1373 * @throws InvocationTargetException if the property accessor method throws an exception
1374 * @throws NoSuchMethodException if an accessor method for this property cannot be found
1375 */
1376 public void setMappedProperty(final Object bean, final String name, final String key, final Object value)
1377 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1378 Objects.requireNonNull(bean, "bean");
1379 Objects.requireNonNull(name, "name");
1380 Objects.requireNonNull(key, "key");
1381 // Handle DynaBean instances specially
1382 if (bean instanceof DynaBean) {
1383 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1384 if (descriptor == null) {
1385 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1386 }
1387 ((DynaBean) bean).set(name, key, value);
1388 return;
1389 }
1390
1391 // Retrieve the property descriptor for the specified property
1392 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1393 if (descriptor == null) {
1394 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
1395 }
1396
1397 if (descriptor instanceof MappedPropertyDescriptor) {
1398 // Call the keyed setter method if there is one
1399 Method mappedWriteMethod = ((MappedPropertyDescriptor) descriptor).getMappedWriteMethod();
1400 mappedWriteMethod = MethodUtils.getAccessibleMethod(bean.getClass(), mappedWriteMethod);
1401 if (mappedWriteMethod == null) {
1402 throw new NoSuchMethodException("Property '" + name + "' has no mapped setter method" + "on bean class '" + bean.getClass() + "'");
1403 }
1404 if (LOG.isTraceEnabled()) {
1405 final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1406 LOG.trace("setSimpleProperty: Invoking method " + mappedWriteMethod + " with key=" + key + ", value=" + value + " (class " + valueClassName
1407 + ")");
1408 }
1409 invokeMethod(mappedWriteMethod, bean, key, value);
1410 } else {
1411 /* means that the result has to be retrieved from a map */
1412 final Method readMethod = getReadMethod(bean.getClass(), descriptor);
1413 if (readMethod == null) {
1414 throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'");
1415 }
1416 final Object invokeResult = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY);
1417 /* test and fetch from the map */
1418 if (invokeResult instanceof Map) {
1419 toPropertyMap(invokeResult).put(key, value);
1420 }
1421 }
1422 }
1423
1424 /**
1425 * Sets the value of the (possibly nested) property of the specified name, for the specified bean, with no type conversions.
1426 * <p>
1427 * Example values for parameter "name" are:
1428 * <ul>
1429 * <li>"a" -- sets the value of property a of the specified bean</li>
1430 * <li>"a.b" -- gets the value of property a of the specified bean, then on that object sets the value of property b.</li>
1431 * <li>"a(key)" -- sets a value of mapped-property a on the specified bean. This effectively means bean.setA("key").</li>
1432 * <li>"a[3]" -- sets a value of indexed-property a on the specified bean. This effectively means bean.setA(3).</li>
1433 * </ul>
1434 *
1435 * @param bean Bean whose property is to be modified
1436 * @param name Possibly nested name of the property to be modified
1437 * @param value Value to which the property is to be set
1438 * @throws IllegalAccessException if the caller does not have access to the property accessor method
1439 * @throws IllegalArgumentException if {@code bean} or {@code name} is null
1440 * @throws IllegalArgumentException if a nested reference to a property returns null
1441 * @throws InvocationTargetException if the property accessor method throws an exception
1442 * @throws NoSuchMethodException if an accessor method for this property cannot be found
1443 */
1444 public void setNestedProperty(Object bean, String name, final Object value)
1445 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1446 Objects.requireNonNull(bean, "bean");
1447 Objects.requireNonNull(name, "name");
1448 // Resolve nested references
1449 while (resolver.hasNested(name)) {
1450 final String next = resolver.next(name);
1451 Object nestedBean = null;
1452 if (bean instanceof Map) {
1453 nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next);
1454 } else if (resolver.isMapped(next)) {
1455 nestedBean = getMappedProperty(bean, next);
1456 } else if (resolver.isIndexed(next)) {
1457 nestedBean = getIndexedProperty(bean, next);
1458 } else {
1459 nestedBean = getSimpleProperty(bean, next);
1460 }
1461 if (nestedBean == null) {
1462 throw new NestedNullException("Null property value for '" + name + "' on bean class '" + bean.getClass() + "'");
1463 }
1464 bean = nestedBean;
1465 name = resolver.remove(name);
1466 }
1467
1468 if (bean instanceof Map) {
1469 setPropertyOfMapBean(toPropertyMap(bean), name, value);
1470 } else if (resolver.isMapped(name)) {
1471 setMappedProperty(bean, name, value);
1472 } else if (resolver.isIndexed(name)) {
1473 setIndexedProperty(bean, name, value);
1474 } else {
1475 setSimpleProperty(bean, name, value);
1476 }
1477 }
1478
1479 /**
1480 * Sets the value of the specified property of the specified bean, no matter which property reference format is used, with no type conversions.
1481 *
1482 * @param bean Bean whose property is to be modified
1483 * @param name Possibly indexed and/or nested name of the property to be modified
1484 * @param value Value to which this property is to be set
1485 * @throws IllegalAccessException if the caller does not have access to the property accessor method
1486 * @throws IllegalArgumentException if {@code bean} or {@code name} is null
1487 * @throws InvocationTargetException if the property accessor method throws an exception
1488 * @throws NoSuchMethodException if an accessor method for this property cannot be found
1489 */
1490 public void setProperty(final Object bean, final String name, final Object value)
1491 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1492 setNestedProperty(bean, name, value);
1493 }
1494
1495 /**
1496 * 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
1497 * Map.
1498 * <p>
1499 * The standard implementation here is to:
1500 * <ul>
1501 * <li>call bean.set(propertyName) for all propertyName values.</li>
1502 * <li>throw an IllegalArgumentException if the property specifier contains MAPPED_DELIM or INDEXED_DELIM, as Map entries are essentially simple properties;
1503 * 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>
1504 * </ul>
1505 * <p>
1506 * 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,
1507 * 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
1508 * 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]
1509 * <p>
1510 * 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
1511 * 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
1512 * method, do not forget to deal with MAPPED_DELIM and INDEXED_DELIM characters in the propertyName.
1513 * <p>
1514 * 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
1515 * 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
1516 * PropertyUtilsBean class by overriding this method.
1517 *
1518 * @param bean Map bean
1519 * @param propertyName The property name
1520 * @param value the property value
1521 * @throws IllegalArgumentException when the propertyName is regarded as being invalid.
1522 * @throws IllegalAccessException just in case subclasses override this method to try to access real setter methods and find permission is denied.
1523 * @throws InvocationTargetException just in case subclasses override this method to try to access real setter methods, and find it throws an exception when
1524 * invoked.
1525 *
1526 * @throws NoSuchMethodException just in case subclasses override this method to try to access real setter methods, and want to fail if no simple method
1527 * is available.
1528 * @since 1.8.0
1529 */
1530 protected void setPropertyOfMapBean(final Map<String, Object> bean, String propertyName, final Object value)
1531 throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1532 if (resolver.isMapped(propertyName)) {
1533 final String name = resolver.getProperty(propertyName);
1534 if (name == null || name.isEmpty()) {
1535 propertyName = resolver.getKey(propertyName);
1536 }
1537 }
1538
1539 if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) {
1540 throw new IllegalArgumentException("Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName);
1541 }
1542
1543 bean.put(propertyName, value);
1544 }
1545
1546 /**
1547 * Configure the {@link Resolver} implementation used by BeanUtils.
1548 * <p>
1549 * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression
1550 * language</em> that BeanUtils recognizes.
1551 * <p>
1552 * {@link DefaultResolver} is the default implementation used.
1553 *
1554 * @param resolver The property expression resolver.
1555 * @since 1.8.0
1556 */
1557 public void setResolver(final Resolver resolver) {
1558 if (resolver == null) {
1559 this.resolver = new DefaultResolver();
1560 } else {
1561 this.resolver = resolver;
1562 }
1563 }
1564
1565 /**
1566 * Sets the value of the specified simple property of the specified bean, with no type conversions.
1567 *
1568 * @param bean Bean whose property is to be modified
1569 * @param name Name of the property to be modified
1570 * @param value Value to which the property should be set
1571 * @throws IllegalAccessException if the caller does not have access to the property accessor method
1572 * @throws IllegalArgumentException if {@code bean} or {@code name} is null
1573 * @throws IllegalArgumentException if the property name is nested or indexed
1574 * @throws InvocationTargetException if the property accessor method throws an exception
1575 * @throws NoSuchMethodException if an accessor method for this property cannot be found
1576 */
1577 public void setSimpleProperty(final Object bean, final String name, final Object value)
1578 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1579 Objects.requireNonNull(bean, "bean");
1580 Objects.requireNonNull(name, "name");
1581 final Class<?> beanClass = bean.getClass();
1582 // Validate the syntax of the property name
1583 if (resolver.hasNested(name)) {
1584 throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1585 }
1586 if (resolver.isIndexed(name)) {
1587 throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1588 }
1589 if (resolver.isMapped(name)) {
1590 throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'");
1591 }
1592
1593 // Handle DynaBean instances specially
1594 if (bean instanceof DynaBean) {
1595 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
1596 if (descriptor == null) {
1597 throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'");
1598 }
1599 ((DynaBean) bean).set(name, value);
1600 return;
1601 }
1602
1603 // Retrieve the property setter method for the specified property
1604 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
1605 if (descriptor == null) {
1606 throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + beanClass + "'");
1607 }
1608 final Method writeMethod = getWriteMethod(beanClass, descriptor);
1609 if (writeMethod == null) {
1610 throw new NoSuchMethodException("Property '" + name + "' has no setter method in class '" + beanClass + "'");
1611 }
1612
1613 // Call the property setter method
1614 if (LOG.isTraceEnabled()) {
1615 final String valueClassName = value == null ? "<null>" : value.getClass().getName();
1616 LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with value " + value + " (class " + valueClassName + ")");
1617 }
1618 invokeMethod(writeMethod, bean, value);
1619 }
1620 }