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 (for example {@code String[]}), this method will return the type of the items within that array. Note that from 807 * Java 8 and newer, this method do not support such index types from items within an Collection, and will instead return the collection type (for example 808 * java.util.List) from the getter method. 809 * </p> 810 * 811 * @param bean Bean for which a property descriptor is requested 812 * @param name Possibly indexed and/or nested name of the property for which a property descriptor is requested 813 * @return The property type 814 * @throws IllegalAccessException if the caller does not have access to the property accessor method 815 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 816 * @throws IllegalArgumentException if a nested reference to a property returns null 817 * @throws InvocationTargetException if the property accessor method throws an exception 818 * @throws NoSuchMethodException if an accessor method for this property cannot be found 819 */ 820 public Class<?> getPropertyType(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 821 Objects.requireNonNull(bean, "bean"); 822 Objects.requireNonNull(name, "name"); 823 // Resolve nested references 824 while (resolver.hasNested(name)) { 825 final String next = resolver.next(name); 826 final Object nestedBean = getProperty(bean, next); 827 if (nestedBean == null) { 828 throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); 829 } 830 bean = nestedBean; 831 name = resolver.remove(name); 832 } 833 834 // Remove any subscript from the final name value 835 name = resolver.getProperty(name); 836 837 // Special handling for DynaBeans 838 if (bean instanceof DynaBean) { 839 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 840 if (descriptor == null) { 841 return null; 842 } 843 final Class<?> type = descriptor.getType(); 844 if (type == null) { 845 return null; 846 } 847 if (type.isArray()) { 848 return type.getComponentType(); 849 } 850 return type; 851 } 852 853 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 854 if (descriptor == null) { 855 return null; 856 } 857 if (descriptor instanceof IndexedPropertyDescriptor) { 858 return ((IndexedPropertyDescriptor) descriptor).getIndexedPropertyType(); 859 } 860 if (descriptor instanceof MappedPropertyDescriptor) { 861 return ((MappedPropertyDescriptor) descriptor).getMappedPropertyType(); 862 } 863 return descriptor.getPropertyType(); 864 } 865 866 /** 867 * <p> 868 * Return the property getter method for this property if accessible from given {@code clazz} (and if there is one at all); otherwise return {@code null}. 869 * </p> 870 * 871 * <p> 872 * <strong>FIXME</strong> - Does not work with DynaBeans. 873 * </p> 874 * 875 * <p> 876 * This fairly low-level method shouldn't be needed for most usecases. However, if you do have to implement something extra, you can improve consistency 877 * with the standard code (for example that of {@link #getProperty getProperty()}) by calling this method instead of using 878 * {@code descriptor.getReadMethod()} directly. 879 * </p> 880 * 881 * @param clazz The class of the read method will be invoked on 882 * @param descriptor Property descriptor to return a getter for 883 * @return The read method 884 * @since 2.0.0 885 */ 886 public Method getReadMethod(final Class<?> clazz, final PropertyDescriptor descriptor) { 887 return MethodUtils.getAccessibleMethod(clazz, descriptor.getReadMethod()); 888 } 889 890 /** 891 * <p> 892 * Return an accessible property getter method for this property, if there is one; otherwise return {@code null}. 893 * </p> 894 * 895 * <p> 896 * <strong>FIXME</strong> - Does not work with DynaBeans. 897 * </p> 898 * 899 * @param descriptor Property descriptor to return a getter for 900 * @return The read method 901 */ 902 public Method getReadMethod(final PropertyDescriptor descriptor) { 903 return MethodUtils.getAccessibleMethod(descriptor.getReadMethod()); 904 } 905 906 /** 907 * Gets the configured {@link Resolver} implementation used by BeanUtils. 908 * <p> 909 * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression 910 * language</em> that BeanUtils recognizes. 911 * <p> 912 * {@link DefaultResolver} is the default implementation used. 913 * 914 * @return resolver The property expression resolver. 915 * @since 1.8.0 916 */ 917 public Resolver getResolver() { 918 return resolver; 919 } 920 921 /** 922 * Gets the value of the specified simple property of the specified bean, with no type conversions. 923 * 924 * @param bean Bean whose property is to be extracted 925 * @param name Name of the property to be extracted 926 * @return The property value 927 * @throws IllegalAccessException if the caller does not have access to the property accessor method 928 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 929 * @throws IllegalArgumentException if the property name is nested or indexed 930 * @throws InvocationTargetException if the property accessor method throws an exception 931 * @throws NoSuchMethodException if an accessor method for this property cannot be found 932 */ 933 public Object getSimpleProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 934 Objects.requireNonNull(bean, "bean"); 935 Objects.requireNonNull(name, "name"); 936 // Validate the syntax of the property name 937 if (resolver.hasNested(name)) { 938 throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); 939 } 940 if (resolver.isIndexed(name)) { 941 throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); 942 } 943 if (resolver.isMapped(name)) { 944 throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); 945 } 946 947 // Handle DynaBean instances specially 948 if (bean instanceof DynaBean) { 949 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 950 if (descriptor == null) { 951 throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'"); 952 } 953 return ((DynaBean) bean).get(name); 954 } 955 956 // Retrieve the property getter method for the specified property 957 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 958 if (descriptor == null) { 959 throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + bean.getClass() + "'"); 960 } 961 final Method readMethod = getReadMethod(bean.getClass(), descriptor); 962 if (readMethod == null) { 963 throw new NoSuchMethodException("Property '" + name + "' has no getter method in class '" + bean.getClass() + "'"); 964 } 965 966 // Call the property getter and return the value 967 return invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY); 968 } 969 970 /** 971 * <p> 972 * Return the property setter method for this property if accessible from given {@code clazz} (and if there is one at all); otherwise return {@code null}. 973 * </p> 974 * 975 * <p> 976 * <strong>FIXME</strong> - Does not work with DynaBeans. 977 * </p> 978 * 979 * <p> 980 * This fairly low-level method shouldn't be needed for most usecases. However, if you do have to implement something extra, you can improve consistency 981 * with the standard code (for example that of {@link #setProperty setProperty()}) by calling this method instead of using 982 * {@code descriptor.getWriteMethod()} directly. 983 * </p> 984 * 985 * @param clazz The class of the read method will be invoked on 986 * @param descriptor Property descriptor to return a setter for 987 * @return The write method 988 * @since 1.9.1 989 */ 990 public Method getWriteMethod(final Class<?> clazz, final PropertyDescriptor descriptor) { 991 final BeanIntrospectionData data = getIntrospectionData(clazz); 992 return MethodUtils.getAccessibleMethod(clazz, data.getWriteMethod(clazz, descriptor)); 993 } 994 995 /** 996 * <p> 997 * Return an accessible property setter method for this property, if there is one; otherwise return {@code null}. 998 * </p> 999 * 1000 * <p> 1001 * <em>Note:</em> This method does not work correctly with custom bean introspection under certain circumstances. It may return {@code null} even if a write 1002 * method is defined for the property in question. Use {@link #getWriteMethod(Class, PropertyDescriptor)} to be sure that the correct result is returned. 1003 * </p> 1004 * <p> 1005 * <strong>FIXME</strong> - Does not work with DynaBeans. 1006 * </p> 1007 * 1008 * @param descriptor Property descriptor to return a setter for 1009 * @return The write method 1010 */ 1011 public Method getWriteMethod(final PropertyDescriptor descriptor) { 1012 return MethodUtils.getAccessibleMethod(descriptor.getWriteMethod()); 1013 } 1014 1015 /** 1016 * Delegates to {@link Method#invoke(Object, Object...)} and handles some unchecked exceptions. 1017 * 1018 * @see Method#invoke(Object, Object...) 1019 */ 1020 private Object invokeMethod(final Method method, final Object bean, final Object... values) throws IllegalAccessException, InvocationTargetException { 1021 Objects.requireNonNull(bean, "bean"); 1022 try { 1023 return method.invoke(bean, values); 1024 } catch (final NullPointerException | IllegalArgumentException cause) { 1025 // JDK 1.3 and JDK 1.4 throw NullPointerException if an argument is 1026 // null for a primitive value (JDK 1.5+ throw IllegalArgumentException) 1027 final StringBuilder valueString = new StringBuilder(); 1028 if (values != null) { 1029 for (int i = 0; i < values.length; i++) { 1030 if (i > 0) { 1031 valueString.append(", "); 1032 } 1033 if (values[i] == null) { 1034 valueString.append("<null>"); 1035 } else { 1036 valueString.append(values[i].getClass().getName()); 1037 } 1038 } 1039 } 1040 final StringBuilder expectedString = new StringBuilder(); 1041 final Class<?>[] parTypes = method.getParameterTypes(); 1042 if (parTypes != null) { 1043 for (int i = 0; i < parTypes.length; i++) { 1044 if (i > 0) { 1045 expectedString.append(", "); 1046 } 1047 expectedString.append(parTypes[i].getName()); 1048 } 1049 } 1050 throw new IllegalArgumentException("Cannot invoke " + method.getDeclaringClass().getName() + "." + method.getName() + " on bean class '" 1051 + bean.getClass() + "' - " + cause.getMessage() 1052 // as per https://issues.apache.org/jira/browse/BEANUTILS-224 1053 + " - had objects of type \"" + valueString + "\" but expected signature \"" + expectedString + "\"", cause); 1054 } 1055 } 1056 1057 /** 1058 * Return {@code true} if the specified property name identifies a readable property on the specified bean; otherwise, return {@code false}. 1059 * 1060 * @param bean Bean to be examined (may be a {@link DynaBean} 1061 * @param name Property name to be evaluated 1062 * @return {@code true} if the property is readable, otherwise {@code false} 1063 * @throws IllegalArgumentException if {@code bean} or {@code name</code> is <code>null} 1064 * @since 1.6 1065 */ 1066 public boolean isReadable(Object bean, String name) { 1067 // Validate method parameters 1068 Objects.requireNonNull(bean, "bean"); 1069 Objects.requireNonNull(name, "name"); 1070 // Resolve nested references 1071 while (resolver.hasNested(name)) { 1072 final String next = resolver.next(name); 1073 Object nestedBean = null; 1074 try { 1075 nestedBean = getProperty(bean, next); 1076 } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 1077 return false; 1078 } 1079 if (nestedBean == null) { 1080 throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); 1081 } 1082 bean = nestedBean; 1083 name = resolver.remove(name); 1084 } 1085 1086 // Remove any subscript from the final name value 1087 name = resolver.getProperty(name); 1088 1089 // Treat WrapDynaBean as special case - may be a write-only property 1090 // (see Jira issue# BEANUTILS-61) 1091 if (bean instanceof WrapDynaBean) { 1092 bean = ((WrapDynaBean) bean).getInstance(); 1093 } 1094 1095 // Return the requested result 1096 if (bean instanceof DynaBean) { 1097 // All DynaBean properties are readable 1098 return ((DynaBean) bean).getDynaClass().getDynaProperty(name) != null; 1099 } 1100 try { 1101 final PropertyDescriptor desc = getPropertyDescriptor(bean, name); 1102 if (desc != null) { 1103 Method readMethod = getReadMethod(bean.getClass(), desc); 1104 if (readMethod == null) { 1105 if (desc instanceof IndexedPropertyDescriptor) { 1106 readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod(); 1107 } else if (desc instanceof MappedPropertyDescriptor) { 1108 readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod(); 1109 } 1110 readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod); 1111 } 1112 return readMethod != null; 1113 } 1114 return false; 1115 } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 1116 return false; 1117 } 1118 } 1119 1120 /** 1121 * Return {@code true} if the specified property name identifies a writable property on the specified bean; otherwise, return {@code false}. 1122 * 1123 * @param bean Bean to be examined (may be a {@link DynaBean} 1124 * @param name Property name to be evaluated 1125 * @return {@code true} if the property is writable, otherwise {@code false} 1126 * @throws IllegalArgumentException if {@code bean} or {@code name</code> is <code>null} 1127 * @since 1.6 1128 */ 1129 public boolean isWriteable(Object bean, String name) { 1130 // Validate method parameters 1131 Objects.requireNonNull(bean, "bean"); 1132 Objects.requireNonNull(name, "name"); 1133 // Resolve nested references 1134 while (resolver.hasNested(name)) { 1135 final String next = resolver.next(name); 1136 Object nestedBean = null; 1137 try { 1138 nestedBean = getProperty(bean, next); 1139 } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 1140 return false; 1141 } 1142 if (nestedBean == null) { 1143 throw new NestedNullException("Null property value for '" + next + "' on bean class '" + bean.getClass() + "'"); 1144 } 1145 bean = nestedBean; 1146 name = resolver.remove(name); 1147 } 1148 1149 // Remove any subscript from the final name value 1150 name = resolver.getProperty(name); 1151 1152 // Treat WrapDynaBean as special case - may be a read-only property 1153 // (see Jira issue# BEANUTILS-61) 1154 if (bean instanceof WrapDynaBean) { 1155 bean = ((WrapDynaBean) bean).getInstance(); 1156 } 1157 1158 // Return the requested result 1159 if (bean instanceof DynaBean) { 1160 // All DynaBean properties are writable 1161 return ((DynaBean) bean).getDynaClass().getDynaProperty(name) != null; 1162 } 1163 try { 1164 final PropertyDescriptor desc = getPropertyDescriptor(bean, name); 1165 if (desc != null) { 1166 Method writeMethod = getWriteMethod(bean.getClass(), desc); 1167 if (writeMethod == null) { 1168 if (desc instanceof IndexedPropertyDescriptor) { 1169 writeMethod = ((IndexedPropertyDescriptor) desc).getIndexedWriteMethod(); 1170 } else if (desc instanceof MappedPropertyDescriptor) { 1171 writeMethod = ((MappedPropertyDescriptor) desc).getMappedWriteMethod(); 1172 } 1173 writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod); 1174 } 1175 return writeMethod != null; 1176 } 1177 return false; 1178 } catch (final IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 1179 return false; 1180 } 1181 } 1182 1183 /** 1184 * Removes the specified {@code BeanIntrospector}. 1185 * 1186 * @param introspector the {@code BeanIntrospector} to be removed 1187 * @return <strong>true</strong> if the {@code BeanIntrospector} existed and could be removed, <strong>false</strong> otherwise 1188 * @since 1.9 1189 */ 1190 public boolean removeBeanIntrospector(final BeanIntrospector introspector) { 1191 return introspectors.remove(introspector); 1192 } 1193 1194 /** 1195 * Resets the {@link BeanIntrospector} objects registered at this instance. After this method was called, only the default {@code BeanIntrospector} is 1196 * registered. 1197 * 1198 * @since 1.9 1199 */ 1200 public final void resetBeanIntrospectors() { 1201 introspectors.clear(); 1202 introspectors.add(DefaultBeanIntrospector.INSTANCE); 1203 introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS); 1204 } 1205 1206 /** 1207 * Sets the value of the specified indexed property of the specified bean, with no type conversions. In addition to supporting the JavaBeans specification, 1208 * this method has been extended to support {@code List} objects as well. 1209 * 1210 * @param bean Bean whose property is to be set 1211 * @param name Simple property name of the property value to be set 1212 * @param index Index of the property value to be set 1213 * @param value Value to which the indexed property element is to be set 1214 * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property 1215 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1216 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1217 * @throws InvocationTargetException if the property accessor method throws an exception 1218 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1219 */ 1220 public void setIndexedProperty(final Object bean, final String name, final int index, final Object value) 1221 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1222 Objects.requireNonNull(bean, "bean"); 1223 if (name == null || name.isEmpty()) { 1224 if (bean.getClass().isArray()) { 1225 Array.set(bean, index, value); 1226 return; 1227 } 1228 if (bean instanceof List) { 1229 final List<Object> list = toObjectList(bean); 1230 list.set(index, value); 1231 return; 1232 } 1233 } 1234 Objects.requireNonNull(name, "name"); 1235 // Handle DynaBean instances specially 1236 if (bean instanceof DynaBean) { 1237 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 1238 if (descriptor == null) { 1239 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); 1240 } 1241 ((DynaBean) bean).set(name, index, value); 1242 return; 1243 } 1244 1245 // Retrieve the property descriptor for the specified property 1246 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 1247 if (descriptor == null) { 1248 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); 1249 } 1250 1251 // Call the indexed setter method if there is one 1252 if (descriptor instanceof IndexedPropertyDescriptor) { 1253 Method writeMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod(); 1254 writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(), writeMethod); 1255 if (writeMethod != null) { 1256 try { 1257 if (LOG.isTraceEnabled()) { 1258 final String valueClassName = value == null ? "<null>" : value.getClass().getName(); 1259 LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with index=" + index + ", value=" + value + " (class " 1260 + valueClassName + ")"); 1261 } 1262 invokeMethod(writeMethod, bean, Integer.valueOf(index), value); 1263 } catch (final InvocationTargetException e) { 1264 if (e.getTargetException() instanceof IndexOutOfBoundsException) { 1265 throw (IndexOutOfBoundsException) e.getTargetException(); 1266 } 1267 throw e; 1268 } 1269 return; 1270 } 1271 } 1272 1273 // Otherwise, the underlying property must be an array or a list 1274 final Method readMethod = getReadMethod(bean.getClass(), descriptor); 1275 if (readMethod == null) { 1276 throw new NoSuchMethodException("Property '" + name + "' has no getter method on bean class '" + bean.getClass() + "'"); 1277 } 1278 1279 // Call the property getter to get the array or list 1280 final Object array = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY); 1281 if (!array.getClass().isArray()) { 1282 if (!(array instanceof List)) { 1283 throw new IllegalArgumentException("Property '" + name + "' is not indexed on bean class '" + bean.getClass() + "'"); 1284 } 1285 // Modify the specified value in the List 1286 final List<Object> list = toObjectList(array); 1287 list.set(index, value); 1288 } else { 1289 // Modify the specified value in the array 1290 Array.set(array, index, value); 1291 } 1292 } 1293 1294 /** 1295 * Sets the value of the specified indexed property of the specified bean, with no type conversions. The zero-relative index of the required value must be 1296 * included (in square brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. In addition to supporting the 1297 * JavaBeans specification, this method has been extended to support {@code List} objects as well. 1298 * 1299 * @param bean Bean whose property is to be modified 1300 * @param name {@code propertyname[index]} of the property value to be modified 1301 * @param value Value to which the specified property element should be set 1302 * @throws IndexOutOfBoundsException if the specified index is outside the valid range for the underlying property 1303 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1304 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1305 * @throws InvocationTargetException if the property accessor method throws an exception 1306 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1307 */ 1308 public void setIndexedProperty(final Object bean, String name, final Object value) 1309 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1310 Objects.requireNonNull(bean, "bean"); 1311 Objects.requireNonNull(name, "name"); 1312 // Identify the index of the requested individual property 1313 int index = -1; 1314 try { 1315 index = resolver.getIndex(name); 1316 } catch (final IllegalArgumentException e) { 1317 throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'"); 1318 } 1319 if (index < 0) { 1320 throw new IllegalArgumentException("Invalid indexed property '" + name + "' on bean class '" + bean.getClass() + "'"); 1321 } 1322 1323 // Isolate the name 1324 name = resolver.getProperty(name); 1325 1326 // Set the specified indexed property value 1327 setIndexedProperty(bean, name, index, value); 1328 } 1329 1330 /** 1331 * Sets the value of the specified mapped property of the specified bean, with no type conversions. The key of the value to set must be included (in 1332 * brackets) as a suffix to the property name, or {@code IllegalArgumentException} will be thrown. 1333 * 1334 * @param bean Bean whose property is to be set 1335 * @param name {@code propertyname(key)} of the property value to be set 1336 * @param value The property value to be set 1337 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1338 * @throws InvocationTargetException if the property accessor method throws an exception 1339 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1340 */ 1341 public void setMappedProperty(final Object bean, String name, final Object value) 1342 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1343 Objects.requireNonNull(bean, "bean"); 1344 Objects.requireNonNull(name, "name"); 1345 1346 // Identify the key of the requested individual property 1347 String key = null; 1348 try { 1349 key = resolver.getKey(name); 1350 } catch (final IllegalArgumentException e) { 1351 throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'"); 1352 } 1353 if (key == null) { 1354 throw new IllegalArgumentException("Invalid mapped property '" + name + "' on bean class '" + bean.getClass() + "'"); 1355 } 1356 1357 // Isolate the name 1358 name = resolver.getProperty(name); 1359 1360 // Request the specified indexed property value 1361 setMappedProperty(bean, name, key, value); 1362 } 1363 1364 /** 1365 * Sets the value of the specified mapped property of the specified bean, with no type conversions. 1366 * 1367 * @param bean Bean whose property is to be set 1368 * @param name Mapped property name of the property value to be set 1369 * @param key Key of the property value to be set 1370 * @param value The property value to be set 1371 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1372 * @throws InvocationTargetException if the property accessor method throws an exception 1373 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1374 */ 1375 public void setMappedProperty(final Object bean, final String name, final String key, final Object value) 1376 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1377 Objects.requireNonNull(bean, "bean"); 1378 Objects.requireNonNull(name, "name"); 1379 Objects.requireNonNull(key, "key"); 1380 // Handle DynaBean instances specially 1381 if (bean instanceof DynaBean) { 1382 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 1383 if (descriptor == null) { 1384 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); 1385 } 1386 ((DynaBean) bean).set(name, key, value); 1387 return; 1388 } 1389 1390 // Retrieve the property descriptor for the specified property 1391 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 1392 if (descriptor == null) { 1393 throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'"); 1394 } 1395 1396 if (descriptor instanceof MappedPropertyDescriptor) { 1397 // Call the keyed setter method if there is one 1398 Method mappedWriteMethod = ((MappedPropertyDescriptor) descriptor).getMappedWriteMethod(); 1399 mappedWriteMethod = MethodUtils.getAccessibleMethod(bean.getClass(), mappedWriteMethod); 1400 if (mappedWriteMethod == null) { 1401 throw new NoSuchMethodException("Property '" + name + "' has no mapped setter method" + "on bean class '" + bean.getClass() + "'"); 1402 } 1403 if (LOG.isTraceEnabled()) { 1404 final String valueClassName = value == null ? "<null>" : value.getClass().getName(); 1405 LOG.trace("setSimpleProperty: Invoking method " + mappedWriteMethod + " with key=" + key + ", value=" + value + " (class " + valueClassName 1406 + ")"); 1407 } 1408 invokeMethod(mappedWriteMethod, bean, key, value); 1409 } else { 1410 /* means that the result has to be retrieved from a map */ 1411 final Method readMethod = getReadMethod(bean.getClass(), descriptor); 1412 if (readMethod == null) { 1413 throw new NoSuchMethodException("Property '" + name + "' has no mapped getter method on bean class '" + bean.getClass() + "'"); 1414 } 1415 final Object invokeResult = invokeMethod(readMethod, bean, BeanUtils.EMPTY_OBJECT_ARRAY); 1416 /* test and fetch from the map */ 1417 if (invokeResult instanceof Map) { 1418 toPropertyMap(invokeResult).put(key, value); 1419 } 1420 } 1421 } 1422 1423 /** 1424 * Sets the value of the (possibly nested) property of the specified name, for the specified bean, with no type conversions. 1425 * <p> 1426 * Example values for parameter "name" are: 1427 * <ul> 1428 * <li>"a" -- sets the value of property a of the specified bean</li> 1429 * <li>"a.b" -- gets the value of property a of the specified bean, then on that object sets the value of property b.</li> 1430 * <li>"a(key)" -- sets a value of mapped-property a on the specified bean. This effectively means bean.setA("key").</li> 1431 * <li>"a[3]" -- sets a value of indexed-property a on the specified bean. This effectively means bean.setA(3).</li> 1432 * </ul> 1433 * 1434 * @param bean Bean whose property is to be modified 1435 * @param name Possibly nested name of the property to be modified 1436 * @param value Value to which the property is to be set 1437 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1438 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1439 * @throws IllegalArgumentException if a nested reference to a property returns null 1440 * @throws InvocationTargetException if the property accessor method throws an exception 1441 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1442 */ 1443 public void setNestedProperty(Object bean, String name, final Object value) 1444 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1445 Objects.requireNonNull(bean, "bean"); 1446 Objects.requireNonNull(name, "name"); 1447 // Resolve nested references 1448 while (resolver.hasNested(name)) { 1449 final String next = resolver.next(name); 1450 Object nestedBean = null; 1451 if (bean instanceof Map) { 1452 nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next); 1453 } else if (resolver.isMapped(next)) { 1454 nestedBean = getMappedProperty(bean, next); 1455 } else if (resolver.isIndexed(next)) { 1456 nestedBean = getIndexedProperty(bean, next); 1457 } else { 1458 nestedBean = getSimpleProperty(bean, next); 1459 } 1460 if (nestedBean == null) { 1461 throw new NestedNullException("Null property value for '" + name + "' on bean class '" + bean.getClass() + "'"); 1462 } 1463 bean = nestedBean; 1464 name = resolver.remove(name); 1465 } 1466 1467 if (bean instanceof Map) { 1468 setPropertyOfMapBean(toPropertyMap(bean), name, value); 1469 } else if (resolver.isMapped(name)) { 1470 setMappedProperty(bean, name, value); 1471 } else if (resolver.isIndexed(name)) { 1472 setIndexedProperty(bean, name, value); 1473 } else { 1474 setSimpleProperty(bean, name, value); 1475 } 1476 } 1477 1478 /** 1479 * Sets the value of the specified property of the specified bean, no matter which property reference format is used, with no type conversions. 1480 * 1481 * @param bean Bean whose property is to be modified 1482 * @param name Possibly indexed and/or nested name of the property to be modified 1483 * @param value Value to which this property is to be set 1484 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1485 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1486 * @throws InvocationTargetException if the property accessor method throws an exception 1487 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1488 */ 1489 public void setProperty(final Object bean, final String name, final Object value) 1490 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1491 setNestedProperty(bean, name, value); 1492 } 1493 1494 /** 1495 * This method is called by method setNestedProperty when the current bean is found to be a Map object, and defines how to deal with setting a property on a 1496 * Map. 1497 * <p> 1498 * The standard implementation here is to: 1499 * <ul> 1500 * <li>call bean.set(propertyName) for all propertyName values.</li> 1501 * <li>throw an IllegalArgumentException if the property specifier contains MAPPED_DELIM or INDEXED_DELIM, as Map entries are essentially simple properties; 1502 * mapping and indexing operations do not make sense when accessing a map (even thought the returned object may be a Map or an Array).</li> 1503 * </ul> 1504 * <p> 1505 * The default behavior of BeanUtils 1.7.1 or later is for assigning to "a.b" to mean a.put(b, obj) always. However the behavior of BeanUtils version 1.6.0, 1506 * 1.6.1, 1.7.0 was for "a.b" to mean a.setB(obj) if such a method existed, and a.put(b, obj) otherwise. In version 1.5 it meant a.put(b, obj) always (ie 1507 * the same as the behavior in the current version). In versions prior to 1.5 it meant a.setB(obj) always. [yes, this is all <em>very</em> unfortunate] 1508 * <p> 1509 * Users who would like to customize the meaning of "a.b" in method setNestedProperty when a is a Map can create a custom subclass of this class and 1510 * override this method to implement the behavior of their choice, such as restoring the pre-1.4 behavior of this class if they wish. When overriding this 1511 * method, do not forget to deal with MAPPED_DELIM and INDEXED_DELIM characters in the propertyName. 1512 * <p> 1513 * Note, however, that the recommended solution for objects that implement Map but want their simple properties to come first is for <em>those</em> objects 1514 * to override their get/put methods to implement that behavior, and <em>not</em> to solve the problem by modifying the default behavior of the 1515 * PropertyUtilsBean class by overriding this method. 1516 * 1517 * @param bean Map bean 1518 * @param propertyName The property name 1519 * @param value the property value 1520 * @throws IllegalArgumentException when the propertyName is regarded as being invalid. 1521 * @throws IllegalAccessException just in case subclasses override this method to try to access real setter methods and find permission is denied. 1522 * @throws InvocationTargetException just in case subclasses override this method to try to access real setter methods, and find it throws an exception when 1523 * invoked. 1524 * 1525 * @throws NoSuchMethodException just in case subclasses override this method to try to access real setter methods, and want to fail if no simple method 1526 * is available. 1527 * @since 1.8.0 1528 */ 1529 protected void setPropertyOfMapBean(final Map<String, Object> bean, String propertyName, final Object value) 1530 throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1531 if (resolver.isMapped(propertyName)) { 1532 final String name = resolver.getProperty(propertyName); 1533 if (name == null || name.isEmpty()) { 1534 propertyName = resolver.getKey(propertyName); 1535 } 1536 } 1537 1538 if (resolver.isIndexed(propertyName) || resolver.isMapped(propertyName)) { 1539 throw new IllegalArgumentException("Indexed or mapped properties are not supported on" + " objects of type Map: " + propertyName); 1540 } 1541 1542 bean.put(propertyName, value); 1543 } 1544 1545 /** 1546 * Configure the {@link Resolver} implementation used by BeanUtils. 1547 * <p> 1548 * The {@link Resolver} handles the <em>property name</em> expressions and the implementation in use effectively controls the dialect of the <em>expression 1549 * language</em> that BeanUtils recognizes. 1550 * <p> 1551 * {@link DefaultResolver} is the default implementation used. 1552 * 1553 * @param resolver The property expression resolver. 1554 * @since 1.8.0 1555 */ 1556 public void setResolver(final Resolver resolver) { 1557 if (resolver == null) { 1558 this.resolver = new DefaultResolver(); 1559 } else { 1560 this.resolver = resolver; 1561 } 1562 } 1563 1564 /** 1565 * Sets the value of the specified simple property of the specified bean, with no type conversions. 1566 * 1567 * @param bean Bean whose property is to be modified 1568 * @param name Name of the property to be modified 1569 * @param value Value to which the property should be set 1570 * @throws IllegalAccessException if the caller does not have access to the property accessor method 1571 * @throws IllegalArgumentException if {@code bean} or {@code name} is null 1572 * @throws IllegalArgumentException if the property name is nested or indexed 1573 * @throws InvocationTargetException if the property accessor method throws an exception 1574 * @throws NoSuchMethodException if an accessor method for this property cannot be found 1575 */ 1576 public void setSimpleProperty(final Object bean, final String name, final Object value) 1577 throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { 1578 Objects.requireNonNull(bean, "bean"); 1579 Objects.requireNonNull(name, "name"); 1580 final Class<?> beanClass = bean.getClass(); 1581 // Validate the syntax of the property name 1582 if (resolver.hasNested(name)) { 1583 throw new IllegalArgumentException("Nested property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'"); 1584 } 1585 if (resolver.isIndexed(name)) { 1586 throw new IllegalArgumentException("Indexed property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'"); 1587 } 1588 if (resolver.isMapped(name)) { 1589 throw new IllegalArgumentException("Mapped property names are not allowed: Property '" + name + "' on bean class '" + beanClass + "'"); 1590 } 1591 1592 // Handle DynaBean instances specially 1593 if (bean instanceof DynaBean) { 1594 final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); 1595 if (descriptor == null) { 1596 throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'"); 1597 } 1598 ((DynaBean) bean).set(name, value); 1599 return; 1600 } 1601 1602 // Retrieve the property setter method for the specified property 1603 final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); 1604 if (descriptor == null) { 1605 throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + beanClass + "'"); 1606 } 1607 final Method writeMethod = getWriteMethod(beanClass, descriptor); 1608 if (writeMethod == null) { 1609 throw new NoSuchMethodException("Property '" + name + "' has no setter method in class '" + beanClass + "'"); 1610 } 1611 1612 // Call the property setter method 1613 if (LOG.isTraceEnabled()) { 1614 final String valueClassName = value == null ? "<null>" : value.getClass().getName(); 1615 LOG.trace("setSimpleProperty: Invoking method " + writeMethod + " with value " + value + " (class " + valueClassName + ")"); 1616 } 1617 invokeMethod(writeMethod, bean, value); 1618 } 1619}