001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.beanutils2; 019 020import java.beans.IndexedPropertyDescriptor; 021import java.beans.IntrospectionException; 022import java.beans.Introspector; 023import java.beans.PropertyDescriptor; 024import java.lang.reflect.Array; 025import java.lang.reflect.InvocationTargetException; 026import java.lang.reflect.Method; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Objects; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.concurrent.CopyOnWriteArrayList; 033 034import org.apache.commons.beanutils2.expression.DefaultResolver; 035import org.apache.commons.beanutils2.expression.Resolver; 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038 039/** 040 * Utility methods for using Java Reflection APIs to facilitate generic property getter and setter operations on Java objects. Much of this code was originally 041 * included in {@code BeanUtils}, but has been separated because of the volume of code involved. 042 * <p> 043 * In general, the objects that are examined and modified using these methods are expected to conform to the property getter and setter method naming 044 * conventions described in the JavaBeans Specification (Version 1.0.1). No data type conversions are performed, and there are no usage of any 045 * {@code PropertyEditor} classes that have been registered, although a convenient way to access the registered classes themselves is included. 046 * <p> 047 * For the purposes of this class, five formats for referencing a particular property value of a bean are defined, with the <em>default</em> layout of an 048 * identifying String in parentheses. However the notation for these formats and how they are resolved is now (since BeanUtils 1.8.0) controlled by the 049 * configured {@link Resolver} implementation: 050 * <ul> 051 * <li><strong>Simple ({@code name})</strong> - The specified {@code name} identifies an individual property of a particular JavaBean. The name of the actual 052 * getter or setter method to be used is determined using standard JavaBeans introspection, so that (unless overridden by a {@code BeanInfo} class, a property 053 * named "xyz" will have a getter method named {@code getXyz()} or (for boolean properties only) {@code isXyz()}, and a setter method named 054 * {@code setXyz()}.</li> 055 * <li><strong>Nested ({@code name1.name2.name3})</strong> The first name element is used to select a property getter, as for simple references above. The 056 * object returned for this property is then consulted, using the same approach, for a property getter for a property named {@code name2}, and so on. The 057 * property value that is ultimately retrieved or modified is the one identified by the last name element.</li> 058 * <li><strong>Indexed ({@code name[index]})</strong> - The underlying property value is assumed to be an array, or this JavaBean is assumed to have indexed 059 * property getter and setter methods. The appropriate (zero-relative) entry in the array is selected. {@code List} objects are now also supported for 060 * read/write. You simply need to define a getter that returns the {@code List}</li> 061 * <li><strong>Mapped ({@code name(key)})</strong> - The JavaBean is assumed to have an property getter and setter methods with an additional attribute of type 062 * {@link String}.</li> 063 * <li><strong>Combined ({@code name1.name2[index].name3(key)})</strong> - Combining mapped, nested, and indexed references is also supported.</li> 064 * </ul> 065 * 066 * @see Resolver 067 * @see PropertyUtils 068 * @since 1.7 069 */ 070public class PropertyUtilsBean { 071 072 /** Log instance */ 073 private static final Log LOG = LogFactory.getLog(PropertyUtilsBean.class); 074 075 /** 076 * Gets the PropertyUtils bean instance. 077 * 078 * @return The PropertyUtils bean instance 079 */ 080 protected static PropertyUtilsBean getInstance() { 081 return BeanUtilsBean.getInstance().getPropertyUtils(); 082 } 083 084 /** 085 * Converts an object to a list of objects. This method is used when dealing with indexed properties. It assumes that indexed properties are stored as lists 086 * of objects. 087 * 088 * @param obj the object to be converted 089 * @return the resulting list of objects 090 */ 091 @SuppressWarnings("unchecked") 092 private static List<Object> toObjectList(final Object obj) { 093 // indexed properties are stored in lists of objects 094 return (List<Object>) obj; 095 } 096 097 /** 098 * Converts an object to a map with property values. This method is used when dealing with mapped properties. It assumes that mapped properties are stored 099 * in a Map<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 (e.g. {@code String[]}), this method will return the type of the items within that array. Note that from Java 8 807 * and newer, this method do not support such index types from items within an Collection, and will instead return the collection type (e.g. java.util.List) 808 * from the getter method. 809 * 810 * @param bean Bean for which a property descriptor is requested 811 * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested 812 * @return The property type 813 * @throws IllegalAccessException if the caller does not have access to the property accessor method 814 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 815 * @throws IllegalArgumentException if a nested reference to a property returns null 816 * @throws InvocationTargetException if the property accessor method throws an exception 817 * @throws NoSuchMethodException if an accessor method for this property cannot be found 818 */ 819 public Class<?> getPropertyType(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 820 Objects.requireNonNull(bean, "bean"); 821 Objects.requireNonNull(name, "name"); 822 // Resolve nested references 823 while (resolver.hasNested(name)) { 824 final String next = resolver.next(name); 825 final Object nestedBean = getProperty(bean, next); 826 if (nestedBean == null) { 827 throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); 828 } 829 bean = nestedBean; 830 name = resolver.remove(name); 831 } 832 833 // Remove any subscript from the final name value 834 name = resolver.getProperty(name); 835 836 // Special handling for DynaBeans 837 if (bean instanceof DynaBean) { 838 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 839 if (descriptor == null) { 840 return null; 841 } 842 final Class<?> type = descriptor.getType(); 843 if (type == null) { 844 return null; 845 } 846 if (type.isArray()) { 847 return type.getComponentType(); 848 } 849 return type; 850 } 851 852 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 853 if (descriptor == null) { 854 return null; 855 } 856 if (descriptor instanceof IndexedPropertyDescriptor) { 857 return ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType(); 858 } 859 if (descriptor instanceof MappedPropertyDescriptor) { 860 return ((MappedPropertyDescriptor) descriptor).getMappedPropertyType(); 861 } 862 return descriptor.getPropertyType(); 863 } 864 865 /** 866 * <p> 867 * 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}. 868 * </p> 869 * 870 * <p> 871 * <strong>FIXME</strong> - Does not work with DynaBeans. 872 * </p> 873 * 874 * <p> 875 * 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 876 * with the standard code (e.g. that of {@link #getProperty getProperty()}) by calling this method instead of using {@code descriptor.getReadMethod()} 877 * directly. 878 * 879 * @param clazz The class of the read method will be invoked on 880 * @param descriptor Property descriptor to return a getter for 881 * @return The read method 882 * @since 2.0.0 883 */ 884 public Method getReadMethod(final Class<?> clazz, final PropertyDescriptor descriptor) { 885 return MethodUtils.getAccessibleMethod(clazz, descriptor.getReadMethod()); 886 } 887 888 /** 889 * <p> 890 * Return an accessible property getter method for this property, if there is one; otherwise return {@code null}. 891 * </p> 892 * 893 * <p> 894 * <strong>FIXME</strong> - Does not work with DynaBeans. 895 * </p> 896 * 897 * @param descriptor Property descriptor to return a getter for 898 * @return The read method 899 */ 900 public Method getReadMethod(final PropertyDescriptor descriptor) { 901 return MethodUtils.getAccessibleMethod(descriptor.getReadMethod()); 902 } 903 904 /** 905 * Gets the configured {@link Resolver} implementation used by BeanUtils. 906 * <p> 907 * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression 908 * language</em> that BeanUtils recognizes. 909 * <p> 910 * {@link DefaultResolver} is the default implementation used. 911 * 912 * @return resolver The property expression resolver. 913 * @since 1.8.0 914 */ 915 public Resolver getResolver() { 916 return resolver; 917 } 918 919 /** 920 * Gets the value of the specified simple property of the specified bean, with no type conversions. 921 * 922 * @param bean Bean whose property is to be extracted 923 * @param name Name of the property to be extracted 924 * @return The property value 925 * @throws IllegalAccessException if the caller does not have access to the property accessor method 926 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 927 * @throws IllegalArgumentException if the property name is nested or indexed 928 * @throws InvocationTargetException if the property accessor method throws an exception 929 * @throws NoSuchMethodException if an accessor method for this property cannot be found 930 */ 931 public Object getSimpleProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 932 Objects.requireNonNull(bean, "bean"); 933 Objects.requireNonNull(name, "name"); 934 // Validate the syntax of the property name 935 if (resolver.hasNested(name)) { 936 throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); 937 } 938 if (resolver.isIndexed(name)) { 939 throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); 940 } 941 if (resolver.isMapped(name)) { 942 throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); 943 } 944 945 // Handle DynaBean instances specially 946 if (bean instanceof DynaBean) { 947 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 948 if (descriptor == null) { 949 throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'"); 950 } 951 return ((DynaBean) bean).get(name); 952 } 953 954 // Retrieve the property getter method for the specified property 955 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 956 if (descriptor == null) { 957 throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + bean.getClass() + "'"); 958 } 959 final Method readMethod = getReadMethod(bean.getClass(), descriptor); 960 if (readMethod == null) { 961 throw new NoSuchMethodException("Property '" + name + "' has no getter method in class '" + bean.getClass() + "'"); 962 } 963 964 // Call the property getter and return the value 965 return invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY); 966 } 967 968 /** 969 * <p> 970 * 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}. 971 * </p> 972 * 973 * <p> 974 * <strong>FIXME</strong> - Does not work with DynaBeans. 975 * </p> 976 * 977 * <p> 978 * 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 979 * with the standard code (e.g. that of {@link #setProperty setProperty()}) by calling this method instead of using {@code descriptor.getWriteMethod()} 980 * directly. 981 * 982 * @param clazz The class of the read method will be invoked on 983 * @param descriptor Property descriptor to return a setter for 984 * @return The write method 985 * @since 1.9.1 986 */ 987 public Method getWriteMethod(final Class<?> clazz, final PropertyDescriptor descriptor) { 988 final BeanIntrospectionData data = getIntrospectionData(clazz); 989 return MethodUtils.getAccessibleMethod(clazz, data.getWriteMethod(clazz, descriptor)); 990 } 991 992 /** 993 * <p> 994 * Return an accessible property setter method for this property, if there is one; otherwise return {@code null}. 995 * </p> 996 * 997 * <p> 998 * <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 999 * method is defined for the property in question. Use {@link #getWriteMethod(Class, PropertyDescriptor)} to be sure that the correct result is returned. 1000 * </p> 1001 * <p> 1002 * <strong>FIXME</strong> - Does not work with DynaBeans. 1003 * </p> 1004 * 1005 * @param descriptor Property descriptor to return a setter for 1006 * @return The write method 1007 */ 1008 public Method getWriteMethod(final PropertyDescriptor descriptor) { 1009 return MethodUtils.getAccessibleMethod(descriptor.getWriteMethod()); 1010 } 1011 1012 /** 1013 * Delegates to {@link Method#invoke(Object, Object...)} and handles some unchecked exceptions. 1014 * 1015 * @see Method#invoke(Object, Object...) 1016 */ 1017 private Object invokeMethod(final Method method, final Object bean, final Object... values) throws IllegalAccessException, InvocationTargetException { 1018 Objects.requireNonNull(bean, "bean"); 1019 try { 1020 return method.invoke(bean, values); 1021 } catch (final NullPointerException | IllegalArgumentException cause) { 1022 // JDK 1.3 and JDK 1.4 throw NullPointerException if an argument is 1023 // null for a primitive value (JDK 1.5+ throw IllegalArgumentException) 1024 final StringBuilder valueString = new StringBuilder(); 1025 if (values != null) { 1026 for (int i = 0; i < values.length; i++) { 1027 if (i > 0) { 1028 valueString.append(", "); 1029 } 1030 if (values[i] == null) { 1031 valueString.append("<null>"); 1032 } else { 1033 valueString.append(values[i].getClass().getName()); 1034 } 1035 } 1036 } 1037 final StringBuilder expectedString = new StringBuilder(); 1038 final Class<?>[] parTypes = method.getParameterTypes(); 1039 if (parTypes != null) { 1040 for (int i = 0; i < parTypes.length; i++) { 1041 if (i > 0) { 1042 expectedString.append(", "); 1043 } 1044 expectedString.append(parTypes[i].getName()); 1045 } 1046 } 1047 throw new IllegalArgumentException("Cannot invoke " + method.getDeclaringClass().getName() + "." + method.getName() + " on bean class '" 1048 + bean.getClass() + "' - " + cause.getMessage() 1049 // as per https://issues.apache.org/jira/browse/BEANUTILS-224 1050 + " - had objects of type \"" + valueString + "\" but expected signature \"" + expectedString + "\"", cause); 1051 } 1052 } 1053 1054 /** 1055 * Return {@code true} if the specified property name identifies a readable property on the specified bean; otherwise, return {@code false}. 1056 * 1057 * @param bean Bean to be examined (may be a {@link DynaBean} 1058 * @param name Property name to be evaluated 1059 * @return {@code true} if the property is readable, otherwise {@code false} 1060 * @throws IllegalArgumentException if {@code bean} or {@code name</code> is <code>null} 1061 * @since 1.6 1062 */ 1063 public boolean isReadable(Object bean, String name) { 1064 // Validate method parameters 1065 Objects.requireNonNull(bean, "bean"); 1066 Objects.requireNonNull(name, "name"); 1067 // Resolve nested references 1068 while (resolver.hasNested(name)) { 1069 final String next = resolver.next(name); 1070 Object nestedBean = null; 1071 try { 1072 nestedBean = getProperty(bean, next); 1073 } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 1074 return false; 1075 } 1076 if (nestedBean == null) { 1077 throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); 1078 } 1079 bean = nestedBean; 1080 name = resolver.remove(name); 1081 } 1082 1083 // Remove any subscript from the final name value 1084 name = resolver.getProperty(name); 1085 1086 // Treat WrapDynaBean as special case - may be a write-only property 1087 // (see Jira issue# BEANUTILS-61) 1088 if (bean instanceof WrapDynaBean) { 1089 bean = ((WrapDynaBean) bean).getInstance(); 1090 } 1091 1092 // Return the requested result 1093 if (bean instanceof DynaBean) { 1094 // All DynaBean properties are readable 1095 return ((DynaBean) bean).getDynaClass().getDynaProperty(name) != null; 1096 } 1097 try { 1098 final PropertyDescriptor desc = getPropertyDescriptor(bean, name); 1099 if (desc != null) { 1100 Method readMethod = getReadMethod(bean.getClass(), desc); 1101 if (readMethod == null) { 1102 if (desc instanceof IndexedPropertyDescriptor) { 1103 readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod(); 1104 } else if (desc instanceof MappedPropertyDescriptor) { 1105 readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod(); 1106 } 1107 readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod); 1108 } 1109 return readMethod != null; 1110 } 1111 return false; 1112 } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 1113 return false; 1114 } 1115 } 1116 1117 /** 1118 * Return {@code true} if the specified property name identifies a writable property on the specified bean; otherwise, return {@code false}. 1119 * 1120 * @param bean Bean to be examined (may be a {@link DynaBean} 1121 * @param name Property name to be evaluated 1122 * @return {@code true} if the property is writable, otherwise {@code false} 1123 * @throws IllegalArgumentException if {@code bean} or {@code name</code> is <code>null} 1124 * @since 1.6 1125 */ 1126 public boolean isWriteable(Object bean, String name) { 1127 // Validate method parameters 1128 Objects.requireNonNull(bean, "bean"); 1129 Objects.requireNonNull(name, "name"); 1130 // Resolve nested references 1131 while (resolver.hasNested(name)) { 1132 final String next = resolver.next(name); 1133 Object nestedBean = null; 1134 try { 1135 nestedBean = getProperty(bean, next); 1136 } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 1137 return false; 1138 } 1139 if (nestedBean == null) { 1140 throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); 1141 } 1142 bean = nestedBean; 1143 name = resolver.remove(name); 1144 } 1145 1146 // Remove any subscript from the final name value 1147 name = resolver.getProperty(name); 1148 1149 // Treat WrapDynaBean as special case - may be a read-only property 1150 // (see Jira issue# BEANUTILS-61) 1151 if (bean instanceof WrapDynaBean) { 1152 bean = ((WrapDynaBean) bean).getInstance(); 1153 } 1154 1155 // Return the requested result 1156 if (bean instanceof DynaBean) { 1157 // All DynaBean properties are writable 1158 return ((DynaBean) bean).getDynaClass().getDynaProperty(name) != null; 1159 } 1160 try { 1161 final PropertyDescriptor desc = getPropertyDescriptor(bean, name); 1162 if (desc != null) { 1163 Method writeMethod = getWriteMethod(bean.getClass(), desc); 1164 if (writeMethod == null) { 1165 if (desc instanceof IndexedPropertyDescriptor) { 1166 writeMethod = ((IndexedPropertyDescriptor) desc).getIndexedWriteMethod(); 1167 } else if (desc instanceof MappedPropertyDescriptor) { 1168 writeMethod = ((MappedPropertyDescriptor) desc).getMappedWriteMethod(); 1169 } 1170 writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod); 1171 } 1172 return writeMethod != null; 1173 } 1174 return false; 1175 } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 1176 return false; 1177 } 1178 } 1179 1180 /** 1181 * Removes the specified {@code BeanIntrospector}. 1182 * 1183 * @param introspector the {@code BeanIntrospector} to be removed 1184 * @return <strong>true</strong> if the {@code BeanIntrospector} existed and could be removed, <strong>false</strong> otherwise 1185 * @since 1.9 1186 */ 1187 public boolean removeBeanIntrospector(final BeanIntrospector introspector) { 1188 return introspectors.remove(introspector); 1189 } 1190 1191 /** 1192 * Resets the {@link BeanIntrospector} objects registered at this instance. After this method was called, only the default {@code BeanIntrospector} is 1193 * registered. 1194 * 1195 * @since 1.9 1196 */ 1197 public final void resetBeanIntrospectors() { 1198 introspectors.clear(); 1199 introspectors.add(DefaultBeanIntrospector.INSTANCE); 1200 introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS); 1201 } 1202 1203 /** 1204 * Sets the value of the specified indexed property of the specified bean, with no type conversions. In addition to supporting the JavaBeans specification, 1205 * this method has been extended to support {@code List} objects as well. 1206 * 1207 * @param bean Bean whose property is to be set 1208 * @param name Simple property name of the property value to be set 1209 * @param index Index of the property value to be set 1210 * @param value Value to which the indexed property element is to be set 1211 * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property 1212 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1213 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1214 * @throws InvocationTargetException if the property accessor method throws an exception 1215 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1216 */ 1217 public void setIndexedProperty(final Object bean, final String name, final int index, final Object value) 1218 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1219 Objects.requireNonNull(bean, "bean"); 1220 if (name == null || name.isEmpty()) { 1221 if (bean.getClass().isArray()) { 1222 Array.set(bean, index, value); 1223 return; 1224 } 1225 if (bean instanceof List) { 1226 final List<Object> list = toObjectList(bean); 1227 list.set(index, value); 1228 return; 1229 } 1230 } 1231 Objects.requireNonNull(name, "name"); 1232 // Handle DynaBean instances specially 1233 if (bean instanceof DynaBean) { 1234 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 1235 if (descriptor == null) { 1236 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); 1237 } 1238 ((DynaBean) bean).set(name, index, value); 1239 return; 1240 } 1241 1242 // Retrieve the property descriptor for the specified property 1243 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 1244 if (descriptor == null) { 1245 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); 1246 } 1247 1248 // Call the indexed setter method if there is one 1249 if (descriptor instanceof IndexedPropertyDescriptor) { 1250 Method writeMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod(); 1251 writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod); 1252 if (writeMethod != null) { 1253 try { 1254 if (LOG.isTraceEnabled()) { 1255 final String valueClassName = value == null ? "<null>" : value.getClass().getName(); 1256 LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with index=" + index + ", value=" + value + " (class " 1257 + valueClassName + ")"); 1258 } 1259 invokeMethod(writeMethod, bean, Integer.valueOf(index), value); 1260 } catch (final InvocationTargetException e) { 1261 if (e.getTargetException() instanceof IndexOutOfBoundsException) { 1262 throw (IndexOutOfBoundsException) e.getTargetException(); 1263 } 1264 throw e; 1265 } 1266 return; 1267 } 1268 } 1269 1270 // Otherwise, the underlying property must be an array or a list 1271 final Method readMethod = getReadMethod(bean.getClass(), descriptor); 1272 if (readMethod == null) { 1273 throw new NoSuchMethodException("Property '" + name + "' has no getter method on bean class '" + bean.getClass() + "'"); 1274 } 1275 1276 // Call the property getter to get the array or list 1277 final Object array = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY); 1278 if (!array.getClass().isArray()) { 1279 if (!(array instanceof List)) { 1280 throw new IllegalArgumentException("Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'"); 1281 } 1282 // Modify the specified value in the List 1283 final List<Object> list = toObjectList(array); 1284 list.set(index, value); 1285 } else { 1286 // Modify the specified value in the array 1287 Array.set(array, index, value); 1288 } 1289 } 1290 1291 /** 1292 * 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 1293 * included (in square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. In addition to supporting the 1294 * JavaBeans specification, this method has been extended to support {@code List} objects as well. 1295 * 1296 * @param bean Bean whose property is to be modified 1297 * @param name {@code propertyname[index]} of the property value to be modified 1298 * @param value Value to which the specified property element should be set 1299 * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property 1300 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1301 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1302 * @throws InvocationTargetException if the property accessor method throws an exception 1303 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1304 */ 1305 public void setIndexedProperty(final Object bean, String name, final Object value) 1306 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1307 Objects.requireNonNull(bean, "bean"); 1308 Objects.requireNonNull(name, "name"); 1309 // Identify the index of the requested individual property 1310 int index = -1; 1311 try { 1312 index = resolver.getIndex(name); 1313 } catch (final IllegalArgumentException e) { 1314 throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'"); 1315 } 1316 if (index < 0) { 1317 throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'"); 1318 } 1319 1320 // Isolate the name 1321 name = resolver.getProperty(name); 1322 1323 // Set the specified indexed property value 1324 setIndexedProperty(bean, name, index, value); 1325 } 1326 1327 /** 1328 * 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 1329 * brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. 1330 * 1331 * @param bean Bean whose property is to be set 1332 * @param name {@code propertyname(key)} of the property value to be set 1333 * @param value The property value to be set 1334 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1335 * @throws InvocationTargetException if the property accessor method throws an exception 1336 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1337 */ 1338 public void setMappedProperty(final Object bean, String name, final Object value) 1339 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1340 Objects.requireNonNull(bean, "bean"); 1341 Objects.requireNonNull(name, "name"); 1342 1343 // Identify the key of the requested individual property 1344 String key = null; 1345 try { 1346 key = resolver.getKey(name); 1347 } catch (final IllegalArgumentException e) { 1348 throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'"); 1349 } 1350 if (key == null) { 1351 throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'"); 1352 } 1353 1354 // Isolate the name 1355 name = resolver.getProperty(name); 1356 1357 // Request the specified indexed property value 1358 setMappedProperty(bean, name, key, value); 1359 } 1360 1361 /** 1362 * Sets the value of the specified mapped property of the specified bean, with no type conversions. 1363 * 1364 * @param bean Bean whose property is to be set 1365 * @param name Mapped property name of the property value to be set 1366 * @param key Key of the property value to be set 1367 * @param value The property value to be set 1368 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1369 * @throws InvocationTargetException if the property accessor method throws an exception 1370 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1371 */ 1372 public void setMappedProperty(final Object bean, final String name, final String key, final Object value) 1373 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1374 Objects.requireNonNull(bean, "bean"); 1375 Objects.requireNonNull(name, "name"); 1376 Objects.requireNonNull(key, "key"); 1377 // Handle DynaBean instances specially 1378 if (bean instanceof DynaBean) { 1379 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 1380 if (descriptor == null) { 1381 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); 1382 } 1383 ((DynaBean) bean).set(name, key, value); 1384 return; 1385 } 1386 1387 // Retrieve the property descriptor for the specified property 1388 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 1389 if (descriptor == null) { 1390 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); 1391 } 1392 1393 if (descriptor instanceof MappedPropertyDescriptor) { 1394 // Call the keyed setter method if there is one 1395 Method mappedWriteMethod = ((MappedPropertyDescriptor) descriptor).getMappedWriteMethod(); 1396 mappedWriteMethod = MethodUtils.getAccessibleMethod(bean.getClass(), mappedWriteMethod); 1397 if (mappedWriteMethod == null) { 1398 throw new NoSuchMethodException("Property '" + name + "' has no mapped setter method" + "on bean class '" + bean.getClass() + "'"); 1399 } 1400 if (LOG.isTraceEnabled()) { 1401 final String valueClassName = value == null ? "<null>" : value.getClass().getName(); 1402 LOG.trace("setSimpleProperty: Invoking method " + mappedWriteMethod + " with key=" + key + ", value=" + value + " (class " + valueClassName 1403 + ")"); 1404 } 1405 invokeMethod(mappedWriteMethod, bean, key, value); 1406 } else { 1407 /* means that the result has to be retrieved from a map */ 1408 final Method readMethod = getReadMethod(bean.getClass(), descriptor); 1409 if (readMethod == null) { 1410 throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'"); 1411 } 1412 final Object invokeResult = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY); 1413 /* test and fetch from the map */ 1414 if (invokeResult instanceof Map) { 1415 toPropertyMap(invokeResult).put(key, value); 1416 } 1417 } 1418 } 1419 1420 /** 1421 * Sets the value of the (possibly nested) property of the specified name, for the specified bean, with no type conversions. 1422 * <p> 1423 * Example values for parameter "name" are: 1424 * <ul> 1425 * <li>"a" -- sets the value of property a of the specified bean</li> 1426 * <li>"a.b" -- gets the value of property a of the specified bean, then on that object sets the value of property b.</li> 1427 * <li>"a(key)" -- sets a value of mapped-property a on the specified bean. This effectively means bean.setA("key").</li> 1428 * <li>"a[3]" -- sets a value of indexed-property a on the specified bean. This effectively means bean.setA(3).</li> 1429 * </ul> 1430 * 1431 * @param bean Bean whose property is to be modified 1432 * @param name Possibly nested name of the property to be modified 1433 * @param value Value to which the property is to be set 1434 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1435 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1436 * @throws IllegalArgumentException if a nested reference to a property returns null 1437 * @throws InvocationTargetException if the property accessor method throws an exception 1438 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1439 */ 1440 public void setNestedProperty(Object bean, String name, final Object value) 1441 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1442 Objects.requireNonNull(bean, "bean"); 1443 Objects.requireNonNull(name, "name"); 1444 // Resolve nested references 1445 while (resolver.hasNested(name)) { 1446 final String next = resolver.next(name); 1447 Object nestedBean = null; 1448 if (bean instanceof Map) { 1449 nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next); 1450 } else if (resolver.isMapped(next)) { 1451 nestedBean = getMappedProperty(bean, next); 1452 } else if (resolver.isIndexed(next)) { 1453 nestedBean = getIndexedProperty(bean, next); 1454 } else { 1455 nestedBean = getSimpleProperty(bean, next); 1456 } 1457 if (nestedBean == null) { 1458 throw new NestedNullException("Null property value for '" + name + "' on bean class '" + bean.getClass() + "'"); 1459 } 1460 bean = nestedBean; 1461 name = resolver.remove(name); 1462 } 1463 1464 if (bean instanceof Map) { 1465 setPropertyOfMapBean(toPropertyMap(bean), name, value); 1466 } else if (resolver.isMapped(name)) { 1467 setMappedProperty(bean, name, value); 1468 } else if (resolver.isIndexed(name)) { 1469 setIndexedProperty(bean, name, value); 1470 } else { 1471 setSimpleProperty(bean, name, value); 1472 } 1473 } 1474 1475 /** 1476 * Sets the value of the specified property of the specified bean, no matter which property reference format is used, with no type conversions. 1477 * 1478 * @param bean Bean whose property is to be modified 1479 * @param name Possibly indexed and/or nested name of the property to be modified 1480 * @param value Value to which this property is to be set 1481 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1482 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1483 * @throws InvocationTargetException if the property accessor method throws an exception 1484 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1485 */ 1486 public void setProperty(final Object bean, final String name, final Object value) 1487 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1488 setNestedProperty(bean, name, value); 1489 } 1490 1491 /** 1492 * 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 1493 * Map. 1494 * <p> 1495 * The standard implementation here is to: 1496 * <ul> 1497 * <li>call bean.set(propertyName) for all propertyName values.</li> 1498 * <li>throw an IllegalArgumentException if the property specifier contains MAPPED_DELIM or INDEXED_DELIM, as Map entries are essentially simple properties; 1499 * 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> 1500 * </ul> 1501 * <p> 1502 * 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, 1503 * 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 1504 * 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] 1505 * <p> 1506 * 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 1507 * 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 1508 * method, do not forget to deal with MAPPED_DELIM and INDEXED_DELIM characters in the propertyName. 1509 * <p> 1510 * 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 1511 * 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 1512 * PropertyUtilsBean class by overriding this method. 1513 * 1514 * @param bean Map bean 1515 * @param propertyName The property name 1516 * @param value the property value 1517 * @throws IllegalArgumentException when the propertyName is regarded as being invalid. 1518 * @throws IllegalAccessException just in case subclasses override this method to try to access real setter methods and find permission is denied. 1519 * @throws InvocationTargetException just in case subclasses override this method to try to access real setter methods, and find it throws an exception when 1520 * invoked. 1521 * 1522 * @throws NoSuchMethodException just in case subclasses override this method to try to access real setter methods, and want to fail if no simple method 1523 * is available. 1524 * @since 1.8.0 1525 */ 1526 protected void setPropertyOfMapBean(final Map<String, Object> bean, String propertyName, final Object value) 1527 throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1528 if (resolver.isMapped(propertyName)) { 1529 final String name = resolver.getProperty(propertyName); 1530 if (name == null || name.isEmpty()) { 1531 propertyName = resolver.getKey(propertyName); 1532 } 1533 } 1534 1535 if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) { 1536 throw new IllegalArgumentException("Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName); 1537 } 1538 1539 bean.put(propertyName, value); 1540 } 1541 1542 /** 1543 * Configure the {@link Resolver} implementation used by BeanUtils. 1544 * <p> 1545 * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression 1546 * language</em> that BeanUtils recognizes. 1547 * <p> 1548 * {@link DefaultResolver} is the default implementation used. 1549 * 1550 * @param resolver The property expression resolver. 1551 * @since 1.8.0 1552 */ 1553 public void setResolver(final Resolver resolver) { 1554 if (resolver == null) { 1555 this.resolver = new DefaultResolver(); 1556 } else { 1557 this.resolver = resolver; 1558 } 1559 } 1560 1561 /** 1562 * Sets the value of the specified simple property of the specified bean, with no type conversions. 1563 * 1564 * @param bean Bean whose property is to be modified 1565 * @param name Name of the property to be modified 1566 * @param value Value to which the property should be set 1567 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1568 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1569 * @throws IllegalArgumentException if the property name is nested or indexed 1570 * @throws InvocationTargetException if the property accessor method throws an exception 1571 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1572 */ 1573 public void setSimpleProperty(final Object bean, final String name, final Object value) 1574 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1575 Objects.requireNonNull(bean, "bean"); 1576 Objects.requireNonNull(name, "name"); 1577 final Class<?> beanClass = bean.getClass(); 1578 // Validate the syntax of the property name 1579 if (resolver.hasNested(name)) { 1580 throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'"); 1581 } 1582 if (resolver.isIndexed(name)) { 1583 throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'"); 1584 } 1585 if (resolver.isMapped(name)) { 1586 throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'"); 1587 } 1588 1589 // Handle DynaBean instances specially 1590 if (bean instanceof DynaBean) { 1591 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 1592 if (descriptor == null) { 1593 throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'"); 1594 } 1595 ((DynaBean) bean).set(name, value); 1596 return; 1597 } 1598 1599 // Retrieve the property setter method for the specified property 1600 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 1601 if (descriptor == null) { 1602 throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + beanClass + "'"); 1603 } 1604 final Method writeMethod = getWriteMethod(beanClass, descriptor); 1605 if (writeMethod == null) { 1606 throw new NoSuchMethodException("Property '" + name + "' has no setter method in class '" + beanClass + "'"); 1607 } 1608 1609 // Call the property setter method 1610 if (LOG.isTraceEnabled()) { 1611 final String valueClassName = value == null ? "<null>" : value.getClass().getName(); 1612 LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with value " + value + " (class " + valueClassName + ")"); 1613 } 1614 invokeMethod(writeMethod, bean, value); 1615 } 1616}