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