001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.beans; 018 019import java.beans.PropertyChangeEvent; 020import java.lang.reflect.Array; 021import java.lang.reflect.InvocationTargetException; 022import java.lang.reflect.UndeclaredThrowableException; 023import java.security.PrivilegedActionException; 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.HashMap; 027import java.util.Iterator; 028import java.util.List; 029import java.util.Map; 030import java.util.Optional; 031import java.util.Set; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035 036import org.springframework.core.CollectionFactory; 037import org.springframework.core.ResolvableType; 038import org.springframework.core.convert.ConversionException; 039import org.springframework.core.convert.ConverterNotFoundException; 040import org.springframework.core.convert.TypeDescriptor; 041import org.springframework.lang.UsesJava8; 042import org.springframework.util.Assert; 043import org.springframework.util.ClassUtils; 044import org.springframework.util.ObjectUtils; 045import org.springframework.util.StringUtils; 046 047/** 048 * A basic {@link ConfigurablePropertyAccessor} that provides the necessary 049 * infrastructure for all typical use cases. 050 * 051 * <p>This accessor will convert collection and array values to the corresponding 052 * target collections or arrays, if necessary. Custom property editors that deal 053 * with collections or arrays can either be written via PropertyEditor's 054 * {@code setValue}, or against a comma-delimited String via {@code setAsText}, 055 * as String arrays are converted in such a format if the array itself is not 056 * assignable. 057 * 058 * @author Juergen Hoeller 059 * @author Stephane Nicoll 060 * @author Rod Johnson 061 * @author Rob Harrop 062 * @since 4.2 063 * @see #registerCustomEditor 064 * @see #setPropertyValues 065 * @see #setPropertyValue 066 * @see #getPropertyValue 067 * @see #getPropertyType 068 * @see BeanWrapper 069 * @see PropertyEditorRegistrySupport 070 */ 071public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyAccessor { 072 073 /** 074 * We'll create a lot of these objects, so we don't want a new logger every time. 075 */ 076 private static final Log logger = LogFactory.getLog(AbstractNestablePropertyAccessor.class); 077 078 private static Class<?> javaUtilOptionalClass = null; 079 080 static { 081 try { 082 javaUtilOptionalClass = 083 ClassUtils.forName("java.util.Optional", AbstractNestablePropertyAccessor.class.getClassLoader()); 084 } 085 catch (ClassNotFoundException ex) { 086 // Java 8 not available - Optional references simply not supported then. 087 } 088 } 089 090 091 private int autoGrowCollectionLimit = Integer.MAX_VALUE; 092 093 Object wrappedObject; 094 095 private String nestedPath = ""; 096 097 Object rootObject; 098 099 /** Map with cached nested Accessors: nested path -> Accessor instance */ 100 private Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors; 101 102 103 /** 104 * Create a new empty accessor. Wrapped instance needs to be set afterwards. 105 * Registers default editors. 106 * @see #setWrappedInstance 107 */ 108 protected AbstractNestablePropertyAccessor() { 109 this(true); 110 } 111 112 /** 113 * Create a new empty accessor. Wrapped instance needs to be set afterwards. 114 * @param registerDefaultEditors whether to register default editors 115 * (can be suppressed if the accessor won't need any type conversion) 116 * @see #setWrappedInstance 117 */ 118 protected AbstractNestablePropertyAccessor(boolean registerDefaultEditors) { 119 if (registerDefaultEditors) { 120 registerDefaultEditors(); 121 } 122 this.typeConverterDelegate = new TypeConverterDelegate(this); 123 } 124 125 /** 126 * Create a new accessor for the given object. 127 * @param object object wrapped by this accessor 128 */ 129 protected AbstractNestablePropertyAccessor(Object object) { 130 registerDefaultEditors(); 131 setWrappedInstance(object); 132 } 133 134 /** 135 * Create a new accessor, wrapping a new instance of the specified class. 136 * @param clazz class to instantiate and wrap 137 */ 138 protected AbstractNestablePropertyAccessor(Class<?> clazz) { 139 registerDefaultEditors(); 140 setWrappedInstance(BeanUtils.instantiateClass(clazz)); 141 } 142 143 /** 144 * Create a new accessor for the given object, 145 * registering a nested path that the object is in. 146 * @param object object wrapped by this accessor 147 * @param nestedPath the nested path of the object 148 * @param rootObject the root object at the top of the path 149 */ 150 protected AbstractNestablePropertyAccessor(Object object, String nestedPath, Object rootObject) { 151 registerDefaultEditors(); 152 setWrappedInstance(object, nestedPath, rootObject); 153 } 154 155 /** 156 * Create a new accessor for the given object, 157 * registering a nested path that the object is in. 158 * @param object object wrapped by this accessor 159 * @param nestedPath the nested path of the object 160 * @param parent the containing accessor (must not be {@code null}) 161 */ 162 protected AbstractNestablePropertyAccessor(Object object, String nestedPath, AbstractNestablePropertyAccessor parent) { 163 setWrappedInstance(object, nestedPath, parent.getWrappedInstance()); 164 setExtractOldValueForEditor(parent.isExtractOldValueForEditor()); 165 setAutoGrowNestedPaths(parent.isAutoGrowNestedPaths()); 166 setAutoGrowCollectionLimit(parent.getAutoGrowCollectionLimit()); 167 setConversionService(parent.getConversionService()); 168 } 169 170 171 /** 172 * Specify a limit for array and collection auto-growing. 173 * <p>Default is unlimited on a plain accessor. 174 */ 175 public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { 176 this.autoGrowCollectionLimit = autoGrowCollectionLimit; 177 } 178 179 /** 180 * Return the limit for array and collection auto-growing. 181 */ 182 public int getAutoGrowCollectionLimit() { 183 return this.autoGrowCollectionLimit; 184 } 185 186 /** 187 * Switch the target object, replacing the cached introspection results only 188 * if the class of the new object is different to that of the replaced object. 189 * @param object the new target object 190 */ 191 public void setWrappedInstance(Object object) { 192 setWrappedInstance(object, "", null); 193 } 194 195 /** 196 * Switch the target object, replacing the cached introspection results only 197 * if the class of the new object is different to that of the replaced object. 198 * @param object the new target object 199 * @param nestedPath the nested path of the object 200 * @param rootObject the root object at the top of the path 201 */ 202 public void setWrappedInstance(Object object, String nestedPath, Object rootObject) { 203 Assert.notNull(object, "Target object must not be null"); 204 if (object.getClass() == javaUtilOptionalClass) { 205 this.wrappedObject = OptionalUnwrapper.unwrap(object); 206 } 207 else { 208 this.wrappedObject = object; 209 } 210 this.nestedPath = (nestedPath != null ? nestedPath : ""); 211 this.rootObject = (!"".equals(this.nestedPath) ? rootObject : this.wrappedObject); 212 this.nestedPropertyAccessors = null; 213 this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject); 214 } 215 216 public final Object getWrappedInstance() { 217 return this.wrappedObject; 218 } 219 220 public final Class<?> getWrappedClass() { 221 return (this.wrappedObject != null ? this.wrappedObject.getClass() : null); 222 } 223 224 /** 225 * Return the nested path of the object wrapped by this accessor. 226 */ 227 public final String getNestedPath() { 228 return this.nestedPath; 229 } 230 231 /** 232 * Return the root object at the top of the path of this accessor. 233 * @see #getNestedPath 234 */ 235 public final Object getRootInstance() { 236 return this.rootObject; 237 } 238 239 /** 240 * Return the class of the root object at the top of the path of this accessor. 241 * @see #getNestedPath 242 */ 243 public final Class<?> getRootClass() { 244 return (this.rootObject != null ? this.rootObject.getClass() : null); 245 } 246 247 @Override 248 public void setPropertyValue(String propertyName, Object value) throws BeansException { 249 AbstractNestablePropertyAccessor nestedPa; 250 try { 251 nestedPa = getPropertyAccessorForPropertyPath(propertyName); 252 } 253 catch (NotReadablePropertyException ex) { 254 throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, 255 "Nested property in path '" + propertyName + "' does not exist", ex); 256 } 257 PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); 258 nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value)); 259 } 260 261 @Override 262 public void setPropertyValue(PropertyValue pv) throws BeansException { 263 PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens; 264 if (tokens == null) { 265 String propertyName = pv.getName(); 266 AbstractNestablePropertyAccessor nestedPa; 267 try { 268 nestedPa = getPropertyAccessorForPropertyPath(propertyName); 269 } 270 catch (NotReadablePropertyException ex) { 271 throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, 272 "Nested property in path '" + propertyName + "' does not exist", ex); 273 } 274 tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); 275 if (nestedPa == this) { 276 pv.getOriginalPropertyValue().resolvedTokens = tokens; 277 } 278 nestedPa.setPropertyValue(tokens, pv); 279 } 280 else { 281 setPropertyValue(tokens, pv); 282 } 283 } 284 285 protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { 286 if (tokens.keys != null) { 287 processKeyedProperty(tokens, pv); 288 } 289 else { 290 processLocalProperty(tokens, pv); 291 } 292 } 293 294 @SuppressWarnings("unchecked") 295 private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) { 296 Object propValue = getPropertyHoldingValue(tokens); 297 String lastKey = tokens.keys[tokens.keys.length - 1]; 298 299 if (propValue.getClass().isArray()) { 300 PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); 301 Class<?> requiredType = propValue.getClass().getComponentType(); 302 int arrayIndex = Integer.parseInt(lastKey); 303 Object oldValue = null; 304 try { 305 if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) { 306 oldValue = Array.get(propValue, arrayIndex); 307 } 308 Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), 309 requiredType, ph.nested(tokens.keys.length)); 310 int length = Array.getLength(propValue); 311 if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { 312 Class<?> componentType = propValue.getClass().getComponentType(); 313 Object newArray = Array.newInstance(componentType, arrayIndex + 1); 314 System.arraycopy(propValue, 0, newArray, 0, length); 315 setPropertyValue(tokens.actualName, newArray); 316 propValue = getPropertyValue(tokens.actualName); 317 } 318 Array.set(propValue, arrayIndex, convertedValue); 319 } 320 catch (IndexOutOfBoundsException ex) { 321 throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, 322 "Invalid array index in property path '" + tokens.canonicalName + "'", ex); 323 } 324 } 325 326 else if (propValue instanceof List) { 327 PropertyHandler ph = getPropertyHandler(tokens.actualName); 328 Class<?> requiredType = ph.getCollectionType(tokens.keys.length); 329 List<Object> list = (List<Object>) propValue; 330 int index = Integer.parseInt(lastKey); 331 Object oldValue = null; 332 if (isExtractOldValueForEditor() && index < list.size()) { 333 oldValue = list.get(index); 334 } 335 Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), 336 requiredType, ph.nested(tokens.keys.length)); 337 int size = list.size(); 338 if (index >= size && index < this.autoGrowCollectionLimit) { 339 for (int i = size; i < index; i++) { 340 try { 341 list.add(null); 342 } 343 catch (NullPointerException ex) { 344 throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, 345 "Cannot set element with index " + index + " in List of size " + 346 size + ", accessed using property path '" + tokens.canonicalName + 347 "': List does not support filling up gaps with null elements"); 348 } 349 } 350 list.add(convertedValue); 351 } 352 else { 353 try { 354 list.set(index, convertedValue); 355 } 356 catch (IndexOutOfBoundsException ex) { 357 throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, 358 "Invalid list index in property path '" + tokens.canonicalName + "'", ex); 359 } 360 } 361 } 362 363 else if (propValue instanceof Map) { 364 PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); 365 Class<?> mapKeyType = ph.getMapKeyType(tokens.keys.length); 366 Class<?> mapValueType = ph.getMapValueType(tokens.keys.length); 367 Map<Object, Object> map = (Map<Object, Object>) propValue; 368 // IMPORTANT: Do not pass full property name in here - property editors 369 // must not kick in for map keys but rather only for map values. 370 TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); 371 Object convertedMapKey = convertIfNecessary(null, null, lastKey, mapKeyType, typeDescriptor); 372 Object oldValue = null; 373 if (isExtractOldValueForEditor()) { 374 oldValue = map.get(convertedMapKey); 375 } 376 // Pass full property name and old value in here, since we want full 377 // conversion ability for map values. 378 Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), 379 mapValueType, ph.nested(tokens.keys.length)); 380 map.put(convertedMapKey, convertedMapValue); 381 } 382 383 else { 384 throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, 385 "Property referenced in indexed property path '" + tokens.canonicalName + 386 "' is neither an array nor a List nor a Map; returned value was [" + propValue + "]"); 387 } 388 } 389 390 private Object getPropertyHoldingValue(PropertyTokenHolder tokens) { 391 // Apply indexes and map keys: fetch value for all keys but the last one. 392 PropertyTokenHolder getterTokens = new PropertyTokenHolder(); 393 getterTokens.canonicalName = tokens.canonicalName; 394 getterTokens.actualName = tokens.actualName; 395 getterTokens.keys = new String[tokens.keys.length - 1]; 396 System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1); 397 398 Object propValue; 399 try { 400 propValue = getPropertyValue(getterTokens); 401 } 402 catch (NotReadablePropertyException ex) { 403 throw new NotWritablePropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, 404 "Cannot access indexed value in property referenced " + 405 "in indexed property path '" + tokens.canonicalName + "'", ex); 406 } 407 408 if (propValue == null) { 409 // null map value case 410 if (isAutoGrowNestedPaths()) { 411 int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); 412 getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex); 413 propValue = setDefaultValue(getterTokens); 414 } 415 else { 416 throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, 417 "Cannot access indexed value in property referenced " + 418 "in indexed property path '" + tokens.canonicalName + "': returned null"); 419 } 420 } 421 return propValue; 422 } 423 424 private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) { 425 PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); 426 if (ph == null || !ph.isWritable()) { 427 if (pv.isOptional()) { 428 if (logger.isDebugEnabled()) { 429 logger.debug("Ignoring optional value for property '" + tokens.actualName + 430 "' - property not found on bean class [" + getRootClass().getName() + "]"); 431 } 432 return; 433 } 434 if (this.suppressNotWritablePropertyException) { 435 // Optimization for common ignoreUnknown=true scenario since the 436 // exception would be caught and swallowed higher up anyway... 437 return; 438 } 439 throw createNotWritablePropertyException(tokens.canonicalName); 440 } 441 442 Object oldValue = null; 443 try { 444 Object originalValue = pv.getValue(); 445 Object valueToApply = originalValue; 446 if (!Boolean.FALSE.equals(pv.conversionNecessary)) { 447 if (pv.isConverted()) { 448 valueToApply = pv.getConvertedValue(); 449 } 450 else { 451 if (isExtractOldValueForEditor() && ph.isReadable()) { 452 try { 453 oldValue = ph.getValue(); 454 } 455 catch (Exception ex) { 456 if (ex instanceof PrivilegedActionException) { 457 ex = ((PrivilegedActionException) ex).getException(); 458 } 459 if (logger.isDebugEnabled()) { 460 logger.debug("Could not read previous value of property '" + 461 this.nestedPath + tokens.canonicalName + "'", ex); 462 } 463 } 464 } 465 valueToApply = convertForProperty( 466 tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor()); 467 } 468 pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue); 469 } 470 ph.setValue(this.wrappedObject, valueToApply); 471 } 472 catch (TypeMismatchException ex) { 473 throw ex; 474 } 475 catch (InvocationTargetException ex) { 476 PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent( 477 this.rootObject, this.nestedPath + tokens.canonicalName, oldValue, pv.getValue()); 478 if (ex.getTargetException() instanceof ClassCastException) { 479 throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), ex.getTargetException()); 480 } 481 else { 482 Throwable cause = ex.getTargetException(); 483 if (cause instanceof UndeclaredThrowableException) { 484 // May happen e.g. with Groovy-generated methods 485 cause = cause.getCause(); 486 } 487 throw new MethodInvocationException(propertyChangeEvent, cause); 488 } 489 } 490 catch (Exception ex) { 491 PropertyChangeEvent pce = new PropertyChangeEvent( 492 this.rootObject, this.nestedPath + tokens.canonicalName, oldValue, pv.getValue()); 493 throw new MethodInvocationException(pce, ex); 494 } 495 } 496 497 @Override 498 public Class<?> getPropertyType(String propertyName) throws BeansException { 499 try { 500 PropertyHandler ph = getPropertyHandler(propertyName); 501 if (ph != null) { 502 return ph.getPropertyType(); 503 } 504 else { 505 // Maybe an indexed/mapped property... 506 Object value = getPropertyValue(propertyName); 507 if (value != null) { 508 return value.getClass(); 509 } 510 // Check to see if there is a custom editor, 511 // which might give an indication on the desired target type. 512 Class<?> editorType = guessPropertyTypeFromEditors(propertyName); 513 if (editorType != null) { 514 return editorType; 515 } 516 } 517 } 518 catch (InvalidPropertyException ex) { 519 // Consider as not determinable. 520 } 521 return null; 522 } 523 524 @Override 525 public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { 526 try { 527 AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); 528 String finalPath = getFinalPath(nestedPa, propertyName); 529 PropertyTokenHolder tokens = getPropertyNameTokens(finalPath); 530 PropertyHandler ph = nestedPa.getLocalPropertyHandler(tokens.actualName); 531 if (ph != null) { 532 if (tokens.keys != null) { 533 if (ph.isReadable() || ph.isWritable()) { 534 return ph.nested(tokens.keys.length); 535 } 536 } 537 else { 538 if (ph.isReadable() || ph.isWritable()) { 539 return ph.toTypeDescriptor(); 540 } 541 } 542 } 543 } 544 catch (InvalidPropertyException ex) { 545 // Consider as not determinable. 546 } 547 return null; 548 } 549 550 @Override 551 public boolean isReadableProperty(String propertyName) { 552 try { 553 PropertyHandler ph = getPropertyHandler(propertyName); 554 if (ph != null) { 555 return ph.isReadable(); 556 } 557 else { 558 // Maybe an indexed/mapped property... 559 getPropertyValue(propertyName); 560 return true; 561 } 562 } 563 catch (InvalidPropertyException ex) { 564 // Cannot be evaluated, so can't be readable. 565 } 566 return false; 567 } 568 569 @Override 570 public boolean isWritableProperty(String propertyName) { 571 try { 572 PropertyHandler ph = getPropertyHandler(propertyName); 573 if (ph != null) { 574 return ph.isWritable(); 575 } 576 else { 577 // Maybe an indexed/mapped property... 578 getPropertyValue(propertyName); 579 return true; 580 } 581 } 582 catch (InvalidPropertyException ex) { 583 // Cannot be evaluated, so can't be writable. 584 } 585 return false; 586 } 587 588 private Object convertIfNecessary(String propertyName, Object oldValue, Object newValue, Class<?> requiredType, 589 TypeDescriptor td) throws TypeMismatchException { 590 try { 591 return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td); 592 } 593 catch (ConverterNotFoundException ex) { 594 PropertyChangeEvent pce = 595 new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); 596 throw new ConversionNotSupportedException(pce, td.getType(), ex); 597 } 598 catch (ConversionException ex) { 599 PropertyChangeEvent pce = 600 new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); 601 throw new TypeMismatchException(pce, requiredType, ex); 602 } 603 catch (IllegalStateException ex) { 604 PropertyChangeEvent pce = 605 new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); 606 throw new ConversionNotSupportedException(pce, requiredType, ex); 607 } 608 catch (IllegalArgumentException ex) { 609 PropertyChangeEvent pce = 610 new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue); 611 throw new TypeMismatchException(pce, requiredType, ex); 612 } 613 } 614 615 protected Object convertForProperty(String propertyName, Object oldValue, Object newValue, TypeDescriptor td) 616 throws TypeMismatchException { 617 618 return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td); 619 } 620 621 @Override 622 public Object getPropertyValue(String propertyName) throws BeansException { 623 AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); 624 PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); 625 return nestedPa.getPropertyValue(tokens); 626 } 627 628 @SuppressWarnings("unchecked") 629 protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { 630 String propertyName = tokens.canonicalName; 631 String actualName = tokens.actualName; 632 PropertyHandler ph = getLocalPropertyHandler(actualName); 633 if (ph == null || !ph.isReadable()) { 634 throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName); 635 } 636 try { 637 Object value = ph.getValue(); 638 if (tokens.keys != null) { 639 if (value == null) { 640 if (isAutoGrowNestedPaths()) { 641 value = setDefaultValue(tokens.actualName); 642 } 643 else { 644 throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, 645 "Cannot access indexed value of property referenced in indexed " + 646 "property path '" + propertyName + "': returned null"); 647 } 648 } 649 String indexedPropertyName = tokens.actualName; 650 // apply indexes and map keys 651 for (int i = 0; i < tokens.keys.length; i++) { 652 String key = tokens.keys[i]; 653 if (value == null) { 654 throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, 655 "Cannot access indexed value of property referenced in indexed " + 656 "property path '" + propertyName + "': returned null"); 657 } 658 else if (value.getClass().isArray()) { 659 int index = Integer.parseInt(key); 660 value = growArrayIfNecessary(value, index, indexedPropertyName); 661 value = Array.get(value, index); 662 } 663 else if (value instanceof List) { 664 int index = Integer.parseInt(key); 665 List<Object> list = (List<Object>) value; 666 growCollectionIfNecessary(list, index, indexedPropertyName, ph, i + 1); 667 value = list.get(index); 668 } 669 else if (value instanceof Set) { 670 // Apply index to Iterator in case of a Set. 671 Set<Object> set = (Set<Object>) value; 672 int index = Integer.parseInt(key); 673 if (index < 0 || index >= set.size()) { 674 throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, 675 "Cannot get element with index " + index + " from Set of size " + 676 set.size() + ", accessed using property path '" + propertyName + "'"); 677 } 678 Iterator<Object> it = set.iterator(); 679 for (int j = 0; it.hasNext(); j++) { 680 Object elem = it.next(); 681 if (j == index) { 682 value = elem; 683 break; 684 } 685 } 686 } 687 else if (value instanceof Map) { 688 Map<Object, Object> map = (Map<Object, Object>) value; 689 Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); 690 // IMPORTANT: Do not pass full property name in here - property editors 691 // must not kick in for map keys but rather only for map values. 692 TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); 693 Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); 694 value = map.get(convertedMapKey); 695 } 696 else { 697 throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, 698 "Property referenced in indexed property path '" + propertyName + 699 "' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]"); 700 } 701 indexedPropertyName += PROPERTY_KEY_PREFIX + key + PROPERTY_KEY_SUFFIX; 702 } 703 } 704 return value; 705 } 706 catch (IndexOutOfBoundsException ex) { 707 throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, 708 "Index of out of bounds in property path '" + propertyName + "'", ex); 709 } 710 catch (NumberFormatException ex) { 711 throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, 712 "Invalid index in property path '" + propertyName + "'", ex); 713 } 714 catch (TypeMismatchException ex) { 715 throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, 716 "Invalid index in property path '" + propertyName + "'", ex); 717 } 718 catch (InvocationTargetException ex) { 719 throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, 720 "Getter for property '" + actualName + "' threw exception", ex); 721 } 722 catch (Exception ex) { 723 throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, 724 "Illegal attempt to get property '" + actualName + "' threw exception", ex); 725 } 726 } 727 728 729 /** 730 * Return the {@link PropertyHandler} for the specified {@code propertyName}, navigating 731 * if necessary. Return {@code null} if not found rather than throwing an exception. 732 * @param propertyName the property to obtain the descriptor for 733 * @return the property descriptor for the specified property, 734 * or {@code null} if not found 735 * @throws BeansException in case of introspection failure 736 */ 737 protected PropertyHandler getPropertyHandler(String propertyName) throws BeansException { 738 Assert.notNull(propertyName, "Property name must not be null"); 739 AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); 740 return nestedPa.getLocalPropertyHandler(getFinalPath(nestedPa, propertyName)); 741 } 742 743 /** 744 * Return a {@link PropertyHandler} for the specified local {@code propertyName}. Only 745 * used to reach a property available in the current context. 746 * @param propertyName the name of a local property 747 * @return the handler for that property or {@code null} if it has not been found 748 */ 749 protected abstract PropertyHandler getLocalPropertyHandler(String propertyName); 750 751 /** 752 * Create a new nested property accessor instance. 753 * Can be overridden in subclasses to create a PropertyAccessor subclass. 754 * @param object object wrapped by this PropertyAccessor 755 * @param nestedPath the nested path of the object 756 * @return the nested PropertyAccessor instance 757 */ 758 protected abstract AbstractNestablePropertyAccessor newNestedPropertyAccessor(Object object, String nestedPath); 759 760 /** 761 * Create a {@link NotWritablePropertyException} for the specified property. 762 */ 763 protected abstract NotWritablePropertyException createNotWritablePropertyException(String propertyName); 764 765 766 private Object growArrayIfNecessary(Object array, int index, String name) { 767 if (!isAutoGrowNestedPaths()) { 768 return array; 769 } 770 int length = Array.getLength(array); 771 if (index >= length && index < this.autoGrowCollectionLimit) { 772 Class<?> componentType = array.getClass().getComponentType(); 773 Object newArray = Array.newInstance(componentType, index + 1); 774 System.arraycopy(array, 0, newArray, 0, length); 775 for (int i = length; i < Array.getLength(newArray); i++) { 776 Array.set(newArray, i, newValue(componentType, null, name)); 777 } 778 setPropertyValue(name, newArray); 779 return getPropertyValue(name); 780 } 781 else { 782 return array; 783 } 784 } 785 786 private void growCollectionIfNecessary(Collection<Object> collection, int index, String name, 787 PropertyHandler ph, int nestingLevel) { 788 789 if (!isAutoGrowNestedPaths()) { 790 return; 791 } 792 int size = collection.size(); 793 if (index >= size && index < this.autoGrowCollectionLimit) { 794 Class<?> elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); 795 if (elementType != null) { 796 for (int i = collection.size(); i < index + 1; i++) { 797 collection.add(newValue(elementType, null, name)); 798 } 799 } 800 } 801 } 802 803 /** 804 * Get the last component of the path. Also works if not nested. 805 * @param pa property accessor to work on 806 * @param nestedPath property path we know is nested 807 * @return last component of the path (the property on the target bean) 808 */ 809 protected String getFinalPath(AbstractNestablePropertyAccessor pa, String nestedPath) { 810 if (pa == this) { 811 return nestedPath; 812 } 813 return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1); 814 } 815 816 /** 817 * Recursively navigate to return a property accessor for the nested property path. 818 * @param propertyPath property path, which may be nested 819 * @return a property accessor for the target bean 820 */ 821 protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) { 822 int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath); 823 // Handle nested properties recursively. 824 if (pos > -1) { 825 String nestedProperty = propertyPath.substring(0, pos); 826 String nestedPath = propertyPath.substring(pos + 1); 827 AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty); 828 return nestedPa.getPropertyAccessorForPropertyPath(nestedPath); 829 } 830 else { 831 return this; 832 } 833 } 834 835 /** 836 * Retrieve a Property accessor for the given nested property. 837 * Create a new one if not found in the cache. 838 * <p>Note: Caching nested PropertyAccessors is necessary now, 839 * to keep registered custom editors for nested properties. 840 * @param nestedProperty property to create the PropertyAccessor for 841 * @return the PropertyAccessor instance, either cached or newly created 842 */ 843 private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) { 844 if (this.nestedPropertyAccessors == null) { 845 this.nestedPropertyAccessors = new HashMap<String, AbstractNestablePropertyAccessor>(); 846 } 847 // Get value of bean property. 848 PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); 849 String canonicalName = tokens.canonicalName; 850 Object value = getPropertyValue(tokens); 851 if (value == null || (value.getClass() == javaUtilOptionalClass && OptionalUnwrapper.isEmpty(value))) { 852 if (isAutoGrowNestedPaths()) { 853 value = setDefaultValue(tokens); 854 } 855 else { 856 throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName); 857 } 858 } 859 860 // Lookup cached sub-PropertyAccessor, create new one if not found. 861 AbstractNestablePropertyAccessor nestedPa = this.nestedPropertyAccessors.get(canonicalName); 862 if (nestedPa == null || nestedPa.getWrappedInstance() != 863 (value.getClass() == javaUtilOptionalClass ? OptionalUnwrapper.unwrap(value) : value)) { 864 if (logger.isTraceEnabled()) { 865 logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'"); 866 } 867 nestedPa = newNestedPropertyAccessor(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR); 868 // Inherit all type-specific PropertyEditors. 869 copyDefaultEditorsTo(nestedPa); 870 copyCustomEditorsTo(nestedPa, canonicalName); 871 this.nestedPropertyAccessors.put(canonicalName, nestedPa); 872 } 873 else { 874 if (logger.isTraceEnabled()) { 875 logger.trace("Using cached nested property accessor for property '" + canonicalName + "'"); 876 } 877 } 878 return nestedPa; 879 } 880 881 private Object setDefaultValue(String propertyName) { 882 PropertyTokenHolder tokens = new PropertyTokenHolder(); 883 tokens.actualName = propertyName; 884 tokens.canonicalName = propertyName; 885 return setDefaultValue(tokens); 886 } 887 888 private Object setDefaultValue(PropertyTokenHolder tokens) { 889 PropertyValue pv = createDefaultPropertyValue(tokens); 890 setPropertyValue(tokens, pv); 891 return getPropertyValue(tokens); 892 } 893 894 private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { 895 TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName); 896 Class<?> type = desc.getType(); 897 if (type == null) { 898 throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, 899 "Could not determine property type for auto-growing a default value"); 900 } 901 Object defaultValue = newValue(type, desc, tokens.canonicalName); 902 return new PropertyValue(tokens.canonicalName, defaultValue); 903 } 904 905 private Object newValue(Class<?> type, TypeDescriptor desc, String name) { 906 try { 907 if (type.isArray()) { 908 Class<?> componentType = type.getComponentType(); 909 // TODO - only handles 2-dimensional arrays 910 if (componentType.isArray()) { 911 Object array = Array.newInstance(componentType, 1); 912 Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0)); 913 return array; 914 } 915 else { 916 return Array.newInstance(componentType, 0); 917 } 918 } 919 else if (Collection.class.isAssignableFrom(type)) { 920 TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null); 921 return CollectionFactory.createCollection(type, (elementDesc != null ? elementDesc.getType() : null), 16); 922 } 923 else if (Map.class.isAssignableFrom(type)) { 924 TypeDescriptor keyDesc = (desc != null ? desc.getMapKeyTypeDescriptor() : null); 925 return CollectionFactory.createMap(type, (keyDesc != null ? keyDesc.getType() : null), 16); 926 } 927 else { 928 return BeanUtils.instantiate(type); 929 } 930 } 931 catch (Throwable ex) { 932 throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, 933 "Could not instantiate property type [" + type.getName() + "] to auto-grow nested property path", ex); 934 } 935 } 936 937 /** 938 * Parse the given property name into the corresponding property name tokens. 939 * @param propertyName the property name to parse 940 * @return representation of the parsed property tokens 941 */ 942 private PropertyTokenHolder getPropertyNameTokens(String propertyName) { 943 PropertyTokenHolder tokens = new PropertyTokenHolder(); 944 String actualName = null; 945 List<String> keys = new ArrayList<String>(2); 946 int searchIndex = 0; 947 while (searchIndex != -1) { 948 int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex); 949 searchIndex = -1; 950 if (keyStart != -1) { 951 int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length()); 952 if (keyEnd != -1) { 953 if (actualName == null) { 954 actualName = propertyName.substring(0, keyStart); 955 } 956 String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); 957 if (key.length() > 1 && (key.startsWith("'") && key.endsWith("'")) || 958 (key.startsWith("\"") && key.endsWith("\""))) { 959 key = key.substring(1, key.length() - 1); 960 } 961 keys.add(key); 962 searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); 963 } 964 } 965 } 966 tokens.actualName = (actualName != null ? actualName : propertyName); 967 tokens.canonicalName = tokens.actualName; 968 if (!keys.isEmpty()) { 969 tokens.canonicalName += PROPERTY_KEY_PREFIX + 970 StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) + 971 PROPERTY_KEY_SUFFIX; 972 tokens.keys = StringUtils.toStringArray(keys); 973 } 974 return tokens; 975 } 976 977 978 @Override 979 public String toString() { 980 StringBuilder sb = new StringBuilder(getClass().getName()); 981 if (this.wrappedObject != null) { 982 sb.append(": wrapping object [").append(ObjectUtils.identityToString(this.wrappedObject)).append("]"); 983 } 984 else { 985 sb.append(": no wrapped object set"); 986 } 987 return sb.toString(); 988 } 989 990 991 /** 992 * A handler for a specific property. 993 */ 994 protected abstract static class PropertyHandler { 995 996 private final Class<?> propertyType; 997 998 private final boolean readable; 999 1000 private final boolean writable; 1001 1002 public PropertyHandler(Class<?> propertyType, boolean readable, boolean writable) { 1003 this.propertyType = propertyType; 1004 this.readable = readable; 1005 this.writable = writable; 1006 } 1007 1008 public Class<?> getPropertyType() { 1009 return this.propertyType; 1010 } 1011 1012 public boolean isReadable() { 1013 return this.readable; 1014 } 1015 1016 public boolean isWritable() { 1017 return this.writable; 1018 } 1019 1020 public abstract TypeDescriptor toTypeDescriptor(); 1021 1022 public abstract ResolvableType getResolvableType(); 1023 1024 public Class<?> getMapKeyType(int nestingLevel) { 1025 return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0); 1026 } 1027 1028 public Class<?> getMapValueType(int nestingLevel) { 1029 return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1); 1030 } 1031 1032 public Class<?> getCollectionType(int nestingLevel) { 1033 return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); 1034 } 1035 1036 public abstract TypeDescriptor nested(int level); 1037 1038 public abstract Object getValue() throws Exception; 1039 1040 public abstract void setValue(Object object, Object value) throws Exception; 1041 } 1042 1043 1044 /** 1045 * Holder class used to store property tokens. 1046 */ 1047 protected static class PropertyTokenHolder { 1048 1049 public String canonicalName; 1050 1051 public String actualName; 1052 1053 public String[] keys; 1054 } 1055 1056 1057 /** 1058 * Inner class to avoid a hard dependency on Java 8. 1059 */ 1060 @UsesJava8 1061 private static class OptionalUnwrapper { 1062 1063 public static Object unwrap(Object optionalObject) { 1064 Optional<?> optional = (Optional<?>) optionalObject; 1065 Assert.isTrue(optional.isPresent(), "Optional value must be present"); 1066 Object result = optional.get(); 1067 Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported"); 1068 return result; 1069 } 1070 1071 public static boolean isEmpty(Object optionalObject) { 1072 return !((Optional<?>) optionalObject).isPresent(); 1073 } 1074 } 1075 1076}