View Javadoc
1   /*
2    * Copyright 2002-2015 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.beans;
18  
19  import java.beans.PropertyChangeEvent;
20  import java.beans.PropertyDescriptor;
21  import java.lang.reflect.Array;
22  import java.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.lang.reflect.Modifier;
25  import java.security.AccessControlContext;
26  import java.security.AccessController;
27  import java.security.PrivilegedAction;
28  import java.security.PrivilegedActionException;
29  import java.security.PrivilegedExceptionAction;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.HashMap;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Optional;
37  import java.util.Set;
38  
39  import org.apache.commons.logging.Log;
40  import org.apache.commons.logging.LogFactory;
41  
42  import org.springframework.core.CollectionFactory;
43  import org.springframework.core.GenericCollectionTypeResolver;
44  import org.springframework.core.convert.ConversionException;
45  import org.springframework.core.convert.ConverterNotFoundException;
46  import org.springframework.core.convert.Property;
47  import org.springframework.core.convert.TypeDescriptor;
48  import org.springframework.lang.UsesJava8;
49  import org.springframework.util.Assert;
50  import org.springframework.util.ClassUtils;
51  import org.springframework.util.ObjectUtils;
52  import org.springframework.util.StringUtils;
53  
54  /**
55   * Default {@link BeanWrapper} implementation that should be sufficient
56   * for all typical use cases. Caches introspection results for efficiency.
57   *
58   * <p>Note: Auto-registers default property editors from the
59   * {@code org.springframework.beans.propertyeditors} package, which apply
60   * in addition to the JDK's standard PropertyEditors. Applications can call
61   * the {@link #registerCustomEditor(Class, java.beans.PropertyEditor)} method
62   * to register an editor for a particular instance (i.e. they are not shared
63   * across the application). See the base class
64   * {@link PropertyEditorRegistrySupport} for details.
65   *
66   * <p>{@code BeanWrapperImpl} will convert collection and array values
67   * to the corresponding target collections or arrays, if necessary. Custom
68   * property editors that deal with collections or arrays can either be
69   * written via PropertyEditor's {@code setValue}, or against a
70   * comma-delimited String via {@code setAsText}, as String arrays are
71   * converted in such a format if the array itself is not assignable.
72   *
73   * <p><b>NOTE: As of Spring 2.5, this is - for almost all purposes - an
74   * internal class.</b> It is just public in order to allow for access from
75   * other framework packages. For standard application access purposes, use the
76   * {@link PropertyAccessorFactory#forBeanPropertyAccess} factory method instead.
77   *
78   * @author Rod Johnson
79   * @author Juergen Hoeller
80   * @author Rob Harrop
81   * @author Stephane Nicoll
82   * @since 15 April 2001
83   * @see #registerCustomEditor
84   * @see #setPropertyValues
85   * @see #setPropertyValue
86   * @see #getPropertyValue
87   * @see #getPropertyType
88   * @see BeanWrapper
89   * @see PropertyEditorRegistrySupport
90   */
91  public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWrapper {
92  
93  	/**
94  	 * We'll create a lot of these objects, so we don't want a new logger every time.
95  	 */
96  	private static final Log logger = LogFactory.getLog(BeanWrapperImpl.class);
97  
98  	private static Class<?> javaUtilOptionalClass = null;
99  
100 	static {
101 		try {
102 			javaUtilOptionalClass =
103 					ClassUtils.forName("java.util.Optional", BeanWrapperImpl.class.getClassLoader());
104 		}
105 		catch (ClassNotFoundException ex) {
106 			// Java 8 not available - Optional references simply not supported then.
107 		}
108 	}
109 
110 
111 	/** The wrapped object */
112 	private Object object;
113 
114 	private String nestedPath = "";
115 
116 	private Object rootObject;
117 
118 	/**
119 	 * The security context used for invoking the property methods
120 	 */
121 	private AccessControlContext acc;
122 
123 	/**
124 	 * Cached introspections results for this object, to prevent encountering
125 	 * the cost of JavaBeans introspection every time.
126 	 */
127 	private CachedIntrospectionResults cachedIntrospectionResults;
128 
129 	/**
130 	 * Map with cached nested BeanWrappers: nested path -> BeanWrapper instance.
131 	 */
132 	private Map<String, BeanWrapperImpl> nestedBeanWrappers;
133 
134 	private int autoGrowCollectionLimit = Integer.MAX_VALUE;
135 
136 
137 	/**
138 	 * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards.
139 	 * Registers default editors.
140 	 * @see #setWrappedInstance
141 	 */
142 	public BeanWrapperImpl() {
143 		this(true);
144 	}
145 
146 	/**
147 	 * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards.
148 	 * @param registerDefaultEditors whether to register default editors
149 	 * (can be suppressed if the BeanWrapper won't need any type conversion)
150 	 * @see #setWrappedInstance
151 	 */
152 	public BeanWrapperImpl(boolean registerDefaultEditors) {
153 		if (registerDefaultEditors) {
154 			registerDefaultEditors();
155 		}
156 		this.typeConverterDelegate = new TypeConverterDelegate(this);
157 	}
158 
159 	/**
160 	 * Create new BeanWrapperImpl for the given object.
161 	 * @param object object wrapped by this BeanWrapper
162 	 */
163 	public BeanWrapperImpl(Object object) {
164 		registerDefaultEditors();
165 		setWrappedInstance(object);
166 	}
167 
168 	/**
169 	 * Create new BeanWrapperImpl, wrapping a new instance of the specified class.
170 	 * @param clazz class to instantiate and wrap
171 	 */
172 	public BeanWrapperImpl(Class<?> clazz) {
173 		registerDefaultEditors();
174 		setWrappedInstance(BeanUtils.instantiateClass(clazz));
175 	}
176 
177 	/**
178 	 * Create new BeanWrapperImpl for the given object,
179 	 * registering a nested path that the object is in.
180 	 * @param object object wrapped by this BeanWrapper
181 	 * @param nestedPath the nested path of the object
182 	 * @param rootObject the root object at the top of the path
183 	 */
184 	public BeanWrapperImpl(Object object, String nestedPath, Object rootObject) {
185 		registerDefaultEditors();
186 		setWrappedInstance(object, nestedPath, rootObject);
187 	}
188 
189 	/**
190 	 * Create new BeanWrapperImpl for the given object,
191 	 * registering a nested path that the object is in.
192 	 * @param object object wrapped by this BeanWrapper
193 	 * @param nestedPath the nested path of the object
194 	 * @param superBw the containing BeanWrapper (must not be {@code null})
195 	 */
196 	private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl superBw) {
197 		setWrappedInstance(object, nestedPath, superBw.getWrappedInstance());
198 		setExtractOldValueForEditor(superBw.isExtractOldValueForEditor());
199 		setAutoGrowNestedPaths(superBw.isAutoGrowNestedPaths());
200 		setAutoGrowCollectionLimit(superBw.getAutoGrowCollectionLimit());
201 		setConversionService(superBw.getConversionService());
202 		setSecurityContext(superBw.acc);
203 	}
204 
205 
206 	//---------------------------------------------------------------------
207 	// Implementation of BeanWrapper interface
208 	//---------------------------------------------------------------------
209 
210 	/**
211 	 * Switch the target object, replacing the cached introspection results only
212 	 * if the class of the new object is different to that of the replaced object.
213 	 * @param object the new target object
214 	 */
215 	public void setWrappedInstance(Object object) {
216 		setWrappedInstance(object, "", null);
217 	}
218 
219 	/**
220 	 * Switch the target object, replacing the cached introspection results only
221 	 * if the class of the new object is different to that of the replaced object.
222 	 * @param object the new target object
223 	 * @param nestedPath the nested path of the object
224 	 * @param rootObject the root object at the top of the path
225 	 */
226 	public void setWrappedInstance(Object object, String nestedPath, Object rootObject) {
227 		Assert.notNull(object, "Bean object must not be null");
228 		if (object.getClass().equals(javaUtilOptionalClass)) {
229 			this.object = OptionalUnwrapper.unwrap(object);
230 		}
231 		else {
232 			this.object = object;
233 		}
234 		this.nestedPath = (nestedPath != null ? nestedPath : "");
235 		this.rootObject = (!"".equals(this.nestedPath) ? rootObject : this.object);
236 		this.nestedBeanWrappers = null;
237 		this.typeConverterDelegate = new TypeConverterDelegate(this, this.object);
238 		setIntrospectionClass(this.object.getClass());
239 	}
240 
241 	@Override
242 	public final Object getWrappedInstance() {
243 		return this.object;
244 	}
245 
246 	@Override
247 	public final Class<?> getWrappedClass() {
248 		return (this.object != null ? this.object.getClass() : null);
249 	}
250 
251 	/**
252 	 * Return the nested path of the object wrapped by this BeanWrapper.
253 	 */
254 	public final String getNestedPath() {
255 		return this.nestedPath;
256 	}
257 
258 	/**
259 	 * Return the root object at the top of the path of this BeanWrapper.
260 	 * @see #getNestedPath
261 	 */
262 	public final Object getRootInstance() {
263 		return this.rootObject;
264 	}
265 
266 	/**
267 	 * Return the class of the root object at the top of the path of this BeanWrapper.
268 	 * @see #getNestedPath
269 	 */
270 	public final Class<?> getRootClass() {
271 		return (this.rootObject != null ? this.rootObject.getClass() : null);
272 	}
273 
274 
275 
276 	/**
277 	 * Specify a limit for array and collection auto-growing.
278 	 * <p>Default is unlimited on a plain BeanWrapper.
279 	 */
280 	@Override
281 	public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
282 		this.autoGrowCollectionLimit = autoGrowCollectionLimit;
283 	}
284 
285 	/**
286 	 * Return the limit for array and collection auto-growing.
287 	 */
288 	@Override
289 	public int getAutoGrowCollectionLimit() {
290 		return this.autoGrowCollectionLimit;
291 	}
292 
293 	/**
294 	 * Set the security context used during the invocation of the wrapped instance methods.
295 	 * Can be null.
296 	 */
297 	public void setSecurityContext(AccessControlContext acc) {
298 		this.acc = acc;
299 	}
300 
301 	/**
302 	 * Return the security context used during the invocation of the wrapped instance methods.
303 	 * Can be null.
304 	 */
305 	public AccessControlContext getSecurityContext() {
306 		return this.acc;
307 	}
308 
309 	/**
310 	 * Set the class to introspect.
311 	 * Needs to be called when the target object changes.
312 	 * @param clazz the class to introspect
313 	 */
314 	protected void setIntrospectionClass(Class<?> clazz) {
315 		if (this.cachedIntrospectionResults != null &&
316 				!clazz.equals(this.cachedIntrospectionResults.getBeanClass())) {
317 			this.cachedIntrospectionResults = null;
318 		}
319 	}
320 
321 	/**
322 	 * Obtain a lazily initializted CachedIntrospectionResults instance
323 	 * for the wrapped object.
324 	 */
325 	private CachedIntrospectionResults getCachedIntrospectionResults() {
326 		Assert.state(this.object != null, "BeanWrapper does not hold a bean instance");
327 		if (this.cachedIntrospectionResults == null) {
328 			this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(getWrappedClass());
329 		}
330 		return this.cachedIntrospectionResults;
331 	}
332 
333 
334 	@Override
335 	public PropertyDescriptor[] getPropertyDescriptors() {
336 		return getCachedIntrospectionResults().getPropertyDescriptors();
337 	}
338 
339 	@Override
340 	public PropertyDescriptor getPropertyDescriptor(String propertyName) throws BeansException {
341 		PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
342 		if (pd == null) {
343 			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
344 					"No property '" + propertyName + "' found");
345 		}
346 		return pd;
347 	}
348 
349 	/**
350 	 * Internal version of {@link #getPropertyDescriptor}:
351 	 * Returns {@code null} if not found rather than throwing an exception.
352 	 * @param propertyName the property to obtain the descriptor for
353 	 * @return the property descriptor for the specified property,
354 	 * or {@code null} if not found
355 	 * @throws BeansException in case of introspection failure
356 	 */
357 	protected PropertyDescriptor getPropertyDescriptorInternal(String propertyName) throws BeansException {
358 		Assert.notNull(propertyName, "Property name must not be null");
359 		BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
360 		return nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(getFinalPath(nestedBw, propertyName));
361 	}
362 
363 	@Override
364 	public Class<?> getPropertyType(String propertyName) throws BeansException {
365 		try {
366 			PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
367 			if (pd != null) {
368 				return pd.getPropertyType();
369 			}
370 			else {
371 				// Maybe an indexed/mapped property...
372 				Object value = getPropertyValue(propertyName);
373 				if (value != null) {
374 					return value.getClass();
375 				}
376 				// Check to see if there is a custom editor,
377 				// which might give an indication on the desired target type.
378 				Class<?> editorType = guessPropertyTypeFromEditors(propertyName);
379 				if (editorType != null) {
380 					return editorType;
381 				}
382 			}
383 		}
384 		catch (InvalidPropertyException ex) {
385 			// Consider as not determinable.
386 		}
387 		return null;
388 	}
389 
390 	@Override
391 	public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException {
392 		try {
393 			BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
394 			String finalPath = getFinalPath(nestedBw, propertyName);
395 			PropertyTokenHolder tokens = getPropertyNameTokens(finalPath);
396 			PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(tokens.actualName);
397 			if (pd != null) {
398 				if (tokens.keys != null) {
399 					if (pd.getReadMethod() != null || pd.getWriteMethod() != null) {
400 						return TypeDescriptor.nested(property(pd), tokens.keys.length);
401 					}
402 				}
403 				else {
404 					if (pd.getReadMethod() != null || pd.getWriteMethod() != null) {
405 						return new TypeDescriptor(property(pd));
406 					}
407 				}
408 			}
409 		}
410 		catch (InvalidPropertyException ex) {
411 			// Consider as not determinable.
412 		}
413 		return null;
414 	}
415 
416 	@Override
417 	public boolean isReadableProperty(String propertyName) {
418 		try {
419 			PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
420 			if (pd != null) {
421 				if (pd.getReadMethod() != null) {
422 					return true;
423 				}
424 			}
425 			else {
426 				// Maybe an indexed/mapped property...
427 				getPropertyValue(propertyName);
428 				return true;
429 			}
430 		}
431 		catch (InvalidPropertyException ex) {
432 			// Cannot be evaluated, so can't be readable.
433 		}
434 		return false;
435 	}
436 
437 	@Override
438 	public boolean isWritableProperty(String propertyName) {
439 		try {
440 			PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
441 			if (pd != null) {
442 				if (pd.getWriteMethod() != null) {
443 					return true;
444 				}
445 			}
446 			else {
447 				// Maybe an indexed/mapped property...
448 				getPropertyValue(propertyName);
449 				return true;
450 			}
451 		}
452 		catch (InvalidPropertyException ex) {
453 			// Cannot be evaluated, so can't be writable.
454 		}
455 		return false;
456 	}
457 
458 	private Object convertIfNecessary(String propertyName, Object oldValue, Object newValue, Class<?> requiredType,
459 			TypeDescriptor td) throws TypeMismatchException {
460 		try {
461 			return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);
462 		}
463 		catch (ConverterNotFoundException ex) {
464 			PropertyChangeEvent pce =
465 					new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue);
466 			throw new ConversionNotSupportedException(pce, td.getType(), ex);
467 		}
468 		catch (ConversionException ex) {
469 			PropertyChangeEvent pce =
470 					new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue);
471 			throw new TypeMismatchException(pce, requiredType, ex);
472 		}
473 		catch (IllegalStateException ex) {
474 			PropertyChangeEvent pce =
475 					new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue);
476 			throw new ConversionNotSupportedException(pce, requiredType, ex);
477 		}
478 		catch (IllegalArgumentException ex) {
479 			PropertyChangeEvent pce =
480 					new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue);
481 			throw new TypeMismatchException(pce, requiredType, ex);
482 		}
483 	}
484 
485 	/**
486 	 * Convert the given value for the specified property to the latter's type.
487 	 * <p>This method is only intended for optimizations in a BeanFactory.
488 	 * Use the {@code convertIfNecessary} methods for programmatic conversion.
489 	 * @param value the value to convert
490 	 * @param propertyName the target property
491 	 * (note that nested or indexed properties are not supported here)
492 	 * @return the new value, possibly the result of type conversion
493 	 * @throws TypeMismatchException if type conversion failed
494 	 */
495 	public Object convertForProperty(Object value, String propertyName) throws TypeMismatchException {
496 		CachedIntrospectionResults cachedIntrospectionResults = getCachedIntrospectionResults();
497 		PropertyDescriptor pd = cachedIntrospectionResults.getPropertyDescriptor(propertyName);
498 		if (pd == null) {
499 			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
500 					"No property '" + propertyName + "' found");
501 		}
502 		TypeDescriptor td = cachedIntrospectionResults.getTypeDescriptor(pd);
503 		if (td == null) {
504 			td = cachedIntrospectionResults.addTypeDescriptor(pd, new TypeDescriptor(property(pd)));
505 		}
506 		return convertForProperty(propertyName, null, value, td);
507 	}
508 
509 	private Object convertForProperty(String propertyName, Object oldValue, Object newValue, TypeDescriptor td)
510 			throws TypeMismatchException {
511 
512 		return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);
513 	}
514 
515 	private Property property(PropertyDescriptor pd) {
516 		GenericTypeAwarePropertyDescriptor typeAware = (GenericTypeAwarePropertyDescriptor) pd;
517 		return new Property(typeAware.getBeanClass(), typeAware.getReadMethod(), typeAware.getWriteMethod(), typeAware.getName());
518 	}
519 
520 
521 	//---------------------------------------------------------------------
522 	// Implementation methods
523 	//---------------------------------------------------------------------
524 
525 	/**
526 	 * Get the last component of the path. Also works if not nested.
527 	 * @param bw BeanWrapper to work on
528 	 * @param nestedPath property path we know is nested
529 	 * @return last component of the path (the property on the target bean)
530 	 */
531 	private String getFinalPath(BeanWrapper bw, String nestedPath) {
532 		if (bw == this) {
533 			return nestedPath;
534 		}
535 		return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1);
536 	}
537 
538 	/**
539 	 * Recursively navigate to return a BeanWrapper for the nested property path.
540 	 * @param propertyPath property property path, which may be nested
541 	 * @return a BeanWrapper for the target bean
542 	 */
543 	protected BeanWrapperImpl getBeanWrapperForPropertyPath(String propertyPath) {
544 		int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
545 		// Handle nested properties recursively.
546 		if (pos > -1) {
547 			String nestedProperty = propertyPath.substring(0, pos);
548 			String nestedPath = propertyPath.substring(pos + 1);
549 			BeanWrapperImpl nestedBw = getNestedBeanWrapper(nestedProperty);
550 			return nestedBw.getBeanWrapperForPropertyPath(nestedPath);
551 		}
552 		else {
553 			return this;
554 		}
555 	}
556 
557 	/**
558 	 * Retrieve a BeanWrapper for the given nested property.
559 	 * Create a new one if not found in the cache.
560 	 * <p>Note: Caching nested BeanWrappers is necessary now,
561 	 * to keep registered custom editors for nested properties.
562 	 * @param nestedProperty property to create the BeanWrapper for
563 	 * @return the BeanWrapper instance, either cached or newly created
564 	 */
565 	private BeanWrapperImpl getNestedBeanWrapper(String nestedProperty) {
566 		if (this.nestedBeanWrappers == null) {
567 			this.nestedBeanWrappers = new HashMap<String, BeanWrapperImpl>();
568 		}
569 		// Get value of bean property.
570 		PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty);
571 		String canonicalName = tokens.canonicalName;
572 		Object value = getPropertyValue(tokens);
573 		if (value == null || (value.getClass().equals(javaUtilOptionalClass) && OptionalUnwrapper.isEmpty(value))) {
574 			if (isAutoGrowNestedPaths()) {
575 				value = setDefaultValue(tokens);
576 			}
577 			else {
578 				throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName);
579 			}
580 		}
581 
582 		// Lookup cached sub-BeanWrapper, create new one if not found.
583 		BeanWrapperImpl nestedBw = this.nestedBeanWrappers.get(canonicalName);
584 		if (nestedBw == null || nestedBw.getWrappedInstance() !=
585 				(value.getClass().equals(javaUtilOptionalClass) ? OptionalUnwrapper.unwrap(value) : value)) {
586 			if (logger.isTraceEnabled()) {
587 				logger.trace("Creating new nested BeanWrapper for property '" + canonicalName + "'");
588 			}
589 			nestedBw = newNestedBeanWrapper(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR);
590 			// Inherit all type-specific PropertyEditors.
591 			copyDefaultEditorsTo(nestedBw);
592 			copyCustomEditorsTo(nestedBw, canonicalName);
593 			this.nestedBeanWrappers.put(canonicalName, nestedBw);
594 		}
595 		else {
596 			if (logger.isTraceEnabled()) {
597 				logger.trace("Using cached nested BeanWrapper for property '" + canonicalName + "'");
598 			}
599 		}
600 		return nestedBw;
601 	}
602 
603 	private Object setDefaultValue(String propertyName) {
604 		PropertyTokenHolder tokens = new PropertyTokenHolder();
605 		tokens.actualName = propertyName;
606 		tokens.canonicalName = propertyName;
607 		return setDefaultValue(tokens);
608 	}
609 
610 	private Object setDefaultValue(PropertyTokenHolder tokens) {
611 		PropertyValue pv = createDefaultPropertyValue(tokens);
612 		setPropertyValue(tokens, pv);
613 		return getPropertyValue(tokens);
614 	}
615 
616 	private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) {
617 		TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName);
618 		Class<?> type = desc.getType();
619 		if (type == null) {
620 			throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName,
621 					"Could not determine property type for auto-growing a default value");
622 		}
623 		Object defaultValue = newValue(type, desc, tokens.canonicalName);
624 		return new PropertyValue(tokens.canonicalName, defaultValue);
625 	}
626 
627 	private Object newValue(Class<?> type, TypeDescriptor desc, String name) {
628 		try {
629 			if (type.isArray()) {
630 				Class<?> componentType = type.getComponentType();
631 				// TODO - only handles 2-dimensional arrays
632 				if (componentType.isArray()) {
633 					Object array = Array.newInstance(componentType, 1);
634 					Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0));
635 					return array;
636 				}
637 				else {
638 					return Array.newInstance(componentType, 0);
639 				}
640 			}
641 			else if (Collection.class.isAssignableFrom(type)) {
642 				TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null);
643 				return CollectionFactory.createCollection(type, (elementDesc != null ? elementDesc.getType() : null), 16);
644 			}
645 			else if (Map.class.isAssignableFrom(type)) {
646 				TypeDescriptor keyDesc = (desc != null ? desc.getMapKeyTypeDescriptor() : null);
647 				return CollectionFactory.createMap(type, (keyDesc != null ? keyDesc.getType() : null), 16);
648 			}
649 			else {
650 				return type.newInstance();
651 			}
652 		}
653 		catch (Exception ex) {
654 			// TODO: Root cause exception context is lost here; just exception message preserved.
655 			// Should we throw another exception type that preserves context instead?
656 			throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name,
657 					"Could not instantiate property type [" + type.getName() + "] to auto-grow nested property path: " + ex);
658 		}
659 	}
660 
661 	/**
662 	 * Create a new nested BeanWrapper instance.
663 	 * <p>Default implementation creates a BeanWrapperImpl instance.
664 	 * Can be overridden in subclasses to create a BeanWrapperImpl subclass.
665 	 * @param object object wrapped by this BeanWrapper
666 	 * @param nestedPath the nested path of the object
667 	 * @return the nested BeanWrapper instance
668 	 */
669 	protected BeanWrapperImpl newNestedBeanWrapper(Object object, String nestedPath) {
670 		return new BeanWrapperImpl(object, nestedPath, this);
671 	}
672 
673 	/**
674 	 * Parse the given property name into the corresponding property name tokens.
675 	 * @param propertyName the property name to parse
676 	 * @return representation of the parsed property tokens
677 	 */
678 	private PropertyTokenHolder getPropertyNameTokens(String propertyName) {
679 		PropertyTokenHolder tokens = new PropertyTokenHolder();
680 		String actualName = null;
681 		List<String> keys = new ArrayList<String>(2);
682 		int searchIndex = 0;
683 		while (searchIndex != -1) {
684 			int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex);
685 			searchIndex = -1;
686 			if (keyStart != -1) {
687 				int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length());
688 				if (keyEnd != -1) {
689 					if (actualName == null) {
690 						actualName = propertyName.substring(0, keyStart);
691 					}
692 					String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd);
693 					if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) {
694 						key = key.substring(1, key.length() - 1);
695 					}
696 					keys.add(key);
697 					searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length();
698 				}
699 			}
700 		}
701 		tokens.actualName = (actualName != null ? actualName : propertyName);
702 		tokens.canonicalName = tokens.actualName;
703 		if (!keys.isEmpty()) {
704 			tokens.canonicalName +=
705 					PROPERTY_KEY_PREFIX +
706 					StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) +
707 					PROPERTY_KEY_SUFFIX;
708 			tokens.keys = StringUtils.toStringArray(keys);
709 		}
710 		return tokens;
711 	}
712 
713 
714 	//---------------------------------------------------------------------
715 	// Implementation of PropertyAccessor interface
716 	//---------------------------------------------------------------------
717 
718 	@Override
719 	public Object getPropertyValue(String propertyName) throws BeansException {
720 		BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
721 		PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName));
722 		return nestedBw.getPropertyValue(tokens);
723 	}
724 
725     @SuppressWarnings("unchecked")
726 	private Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException {
727 		String propertyName = tokens.canonicalName;
728 		String actualName = tokens.actualName;
729 		PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
730 		if (pd == null || pd.getReadMethod() == null) {
731 			throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName);
732 		}
733 		final Method readMethod = pd.getReadMethod();
734 		try {
735 			if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers()) && !readMethod.isAccessible()) {
736 				if (System.getSecurityManager() != null) {
737 					AccessController.doPrivileged(new PrivilegedAction<Object>() {
738 						@Override
739 						public Object run() {
740 							readMethod.setAccessible(true);
741 							return null;
742 						}
743 					});
744 				}
745 				else {
746 					readMethod.setAccessible(true);
747 				}
748 			}
749 
750 			Object value;
751 			if (System.getSecurityManager() != null) {
752 				try {
753 					value = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
754 						@Override
755 						public Object run() throws Exception {
756 							return readMethod.invoke(object, (Object[]) null);
757 						}
758 					}, acc);
759 				}
760 				catch (PrivilegedActionException pae) {
761 					throw pae.getException();
762 				}
763 			}
764 			else {
765 				value = readMethod.invoke(object, (Object[]) null);
766 			}
767 
768 			if (tokens.keys != null) {
769 				if (value == null) {
770 					if (isAutoGrowNestedPaths()) {
771 						value = setDefaultValue(tokens.actualName);
772 					}
773 					else {
774 						throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
775 								"Cannot access indexed value of property referenced in indexed " +
776 								"property path '" + propertyName + "': returned null");
777 					}
778 				}
779 				String indexedPropertyName = tokens.actualName;
780 				// apply indexes and map keys
781 				for (int i = 0; i < tokens.keys.length; i++) {
782 					String key = tokens.keys[i];
783 					if (value == null) {
784 						throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
785 								"Cannot access indexed value of property referenced in indexed " +
786 								"property path '" + propertyName + "': returned null");
787 					}
788 					else if (value.getClass().isArray()) {
789 						int index = Integer.parseInt(key);
790 						value = growArrayIfNecessary(value, index, indexedPropertyName);
791 						value = Array.get(value, index);
792 					}
793 					else if (value instanceof List) {
794 						int index = Integer.parseInt(key);
795 						List<Object> list = (List<Object>) value;
796 						growCollectionIfNecessary(list, index, indexedPropertyName, pd, i + 1);
797 						value = list.get(index);
798 					}
799 					else if (value instanceof Set) {
800 						// Apply index to Iterator in case of a Set.
801 						Set<Object> set = (Set<Object>) value;
802 						int index = Integer.parseInt(key);
803 						if (index < 0 || index >= set.size()) {
804 							throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
805 									"Cannot get element with index " + index + " from Set of size " +
806 									set.size() + ", accessed using property path '" + propertyName + "'");
807 						}
808 						Iterator<Object> it = set.iterator();
809 						for (int j = 0; it.hasNext(); j++) {
810 							Object elem = it.next();
811 							if (j == index) {
812 								value = elem;
813 								break;
814 							}
815 						}
816 					}
817 					else if (value instanceof Map) {
818 						Map<Object, Object> map = (Map<Object, Object>) value;
819 						Class<?> mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType(pd.getReadMethod(), i + 1);
820 						// IMPORTANT: Do not pass full property name in here - property editors
821 						// must not kick in for map keys but rather only for map values.
822 						TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
823 						Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
824 						value = map.get(convertedMapKey);
825 					}
826 					else {
827 						throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
828 								"Property referenced in indexed property path '" + propertyName +
829 								"' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]");
830 					}
831 					indexedPropertyName += PROPERTY_KEY_PREFIX + key + PROPERTY_KEY_SUFFIX;
832 				}
833 			}
834 			return value;
835 		}
836 		catch (IndexOutOfBoundsException ex) {
837 			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
838 					"Index of out of bounds in property path '" + propertyName + "'", ex);
839 		}
840 		catch (NumberFormatException ex) {
841 			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
842 					"Invalid index in property path '" + propertyName + "'", ex);
843 		}
844 		catch (TypeMismatchException ex) {
845 			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
846 					"Invalid index in property path '" + propertyName + "'", ex);
847 		}
848 		catch (InvocationTargetException ex) {
849 			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
850 					"Getter for property '" + actualName + "' threw exception", ex);
851 		}
852 		catch (Exception ex) {
853 			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
854 					"Illegal attempt to get property '" + actualName + "' threw exception", ex);
855 		}
856 	}
857 
858 	private Object growArrayIfNecessary(Object array, int index, String name) {
859 		if (!isAutoGrowNestedPaths()) {
860 			return array;
861 		}
862 		int length = Array.getLength(array);
863 		if (index >= length && index < this.autoGrowCollectionLimit) {
864 			Class<?> componentType = array.getClass().getComponentType();
865 			Object newArray = Array.newInstance(componentType, index + 1);
866 			System.arraycopy(array, 0, newArray, 0, length);
867 			for (int i = length; i < Array.getLength(newArray); i++) {
868 				Array.set(newArray, i, newValue(componentType, null, name));
869 			}
870 			// TODO this is not efficient because conversion may create a copy ... set directly because we know it is assignable.
871 			setPropertyValue(name, newArray);
872 			return getPropertyValue(name);
873 		}
874 		else {
875 			return array;
876 		}
877 	}
878 
879 	private void growCollectionIfNecessary(Collection<Object> collection, int index, String name,
880 			PropertyDescriptor pd, int nestingLevel) {
881 
882 		if (!isAutoGrowNestedPaths()) {
883 			return;
884 		}
885 		int size = collection.size();
886 		if (index >= size && index < this.autoGrowCollectionLimit) {
887 			Class<?> elementType = GenericCollectionTypeResolver.getCollectionReturnType(pd.getReadMethod(), nestingLevel);
888 			if (elementType != null) {
889 				for (int i = collection.size(); i < index + 1; i++) {
890 					collection.add(newValue(elementType, null, name));
891 				}
892 			}
893 		}
894 	}
895 
896 	@Override
897 	public void setPropertyValue(String propertyName, Object value) throws BeansException {
898 		BeanWrapperImpl nestedBw;
899 		try {
900 			nestedBw = getBeanWrapperForPropertyPath(propertyName);
901 		}
902 		catch (NotReadablePropertyException ex) {
903 			throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
904 					"Nested property in path '" + propertyName + "' does not exist", ex);
905 		}
906 		PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName));
907 		nestedBw.setPropertyValue(tokens, new PropertyValue(propertyName, value));
908 	}
909 
910 	@Override
911 	public void setPropertyValue(PropertyValue pv) throws BeansException {
912 		PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens;
913 		if (tokens == null) {
914 			String propertyName = pv.getName();
915 			BeanWrapperImpl nestedBw;
916 			try {
917 				nestedBw = getBeanWrapperForPropertyPath(propertyName);
918 			}
919 			catch (NotReadablePropertyException ex) {
920 				throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
921 						"Nested property in path '" + propertyName + "' does not exist", ex);
922 			}
923 			tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName));
924 			if (nestedBw == this) {
925 				pv.getOriginalPropertyValue().resolvedTokens = tokens;
926 			}
927 			nestedBw.setPropertyValue(tokens, pv);
928 		}
929 		else {
930 			setPropertyValue(tokens, pv);
931 		}
932 	}
933 
934 	@SuppressWarnings("unchecked")
935 	private void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
936 		String propertyName = tokens.canonicalName;
937 		String actualName = tokens.actualName;
938 
939 		if (tokens.keys != null) {
940 			// Apply indexes and map keys: fetch value for all keys but the last one.
941 			PropertyTokenHolder getterTokens = new PropertyTokenHolder();
942 			getterTokens.canonicalName = tokens.canonicalName;
943 			getterTokens.actualName = tokens.actualName;
944 			getterTokens.keys = new String[tokens.keys.length - 1];
945 			System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1);
946 			Object propValue;
947 			try {
948 				propValue = getPropertyValue(getterTokens);
949 			}
950 			catch (NotReadablePropertyException ex) {
951 				throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
952 						"Cannot access indexed value in property referenced " +
953 						"in indexed property path '" + propertyName + "'", ex);
954 			}
955 			// Set value for last key.
956 			String key = tokens.keys[tokens.keys.length - 1];
957 			if (propValue == null) {
958 				// null map value case
959 				if (isAutoGrowNestedPaths()) {
960 					// TODO: cleanup, this is pretty hacky
961 					int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
962 					getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex);
963 					propValue = setDefaultValue(getterTokens);
964 				}
965 				else {
966 					throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
967 							"Cannot access indexed value in property referenced " +
968 							"in indexed property path '" + propertyName + "': returned null");
969 				}
970 			}
971 			if (propValue.getClass().isArray()) {
972 				PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
973 				Class<?> requiredType = propValue.getClass().getComponentType();
974 				int arrayIndex = Integer.parseInt(key);
975 				Object oldValue = null;
976 				try {
977 					if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) {
978 						oldValue = Array.get(propValue, arrayIndex);
979 					}
980 					Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(),
981 							requiredType, TypeDescriptor.nested(property(pd), tokens.keys.length));
982 					int length = Array.getLength(propValue);
983 					if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) {
984 						Class<?> componentType = propValue.getClass().getComponentType();
985 						Object newArray = Array.newInstance(componentType, arrayIndex + 1);
986 						System.arraycopy(propValue, 0, newArray, 0, length);
987 						setPropertyValue(actualName, newArray);
988 						propValue = getPropertyValue(actualName);
989 					}
990 					Array.set(propValue, arrayIndex, convertedValue);
991 				}
992 				catch (IndexOutOfBoundsException ex) {
993 					throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
994 							"Invalid array index in property path '" + propertyName + "'", ex);
995 				}
996 			}
997 			else if (propValue instanceof List) {
998 				PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
999 				Class<?> requiredType = GenericCollectionTypeResolver.getCollectionReturnType(
1000 						pd.getReadMethod(), tokens.keys.length);
1001 				List<Object> list = (List<Object>) propValue;
1002 				int index = Integer.parseInt(key);
1003 				Object oldValue = null;
1004 				if (isExtractOldValueForEditor() && index < list.size()) {
1005 					oldValue = list.get(index);
1006 				}
1007 				Object convertedValue = convertIfNecessary(propertyName, oldValue, pv.getValue(),
1008 						requiredType, TypeDescriptor.nested(property(pd), tokens.keys.length));
1009 				int size = list.size();
1010 				if (index >= size && index < this.autoGrowCollectionLimit) {
1011 					for (int i = size; i < index; i++) {
1012 						try {
1013 							list.add(null);
1014 						}
1015 						catch (NullPointerException ex) {
1016 							throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
1017 									"Cannot set element with index " + index + " in List of size " +
1018 									size + ", accessed using property path '" + propertyName +
1019 									"': List does not support filling up gaps with null elements");
1020 						}
1021 					}
1022 					list.add(convertedValue);
1023 				}
1024 				else {
1025 					try {
1026 						list.set(index, convertedValue);
1027 					}
1028 					catch (IndexOutOfBoundsException ex) {
1029 						throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
1030 								"Invalid list index in property path '" + propertyName + "'", ex);
1031 					}
1032 				}
1033 			}
1034 			else if (propValue instanceof Map) {
1035 				PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
1036 				Class<?> mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType(
1037 						pd.getReadMethod(), tokens.keys.length);
1038 				Class<?> mapValueType = GenericCollectionTypeResolver.getMapValueReturnType(
1039 						pd.getReadMethod(), tokens.keys.length);
1040 				Map<Object, Object> map = (Map<Object, Object>) propValue;
1041 				// IMPORTANT: Do not pass full property name in here - property editors
1042 				// must not kick in for map keys but rather only for map values.
1043 				TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
1044 				Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
1045 				Object oldValue = null;
1046 				if (isExtractOldValueForEditor()) {
1047 					oldValue = map.get(convertedMapKey);
1048 				}
1049 				// Pass full property name and old value in here, since we want full
1050 				// conversion ability for map values.
1051 				Object convertedMapValue = convertIfNecessary(propertyName, oldValue, pv.getValue(),
1052 						mapValueType, TypeDescriptor.nested(property(pd), tokens.keys.length));
1053 				map.put(convertedMapKey, convertedMapValue);
1054 			}
1055 			else {
1056 				throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
1057 						"Property referenced in indexed property path '" + propertyName +
1058 						"' is neither an array nor a List nor a Map; returned value was [" + pv.getValue() + "]");
1059 			}
1060 		}
1061 
1062 		else {
1063 			PropertyDescriptor pd = pv.resolvedDescriptor;
1064 			if (pd == null || !pd.getWriteMethod().getDeclaringClass().isInstance(this.object)) {
1065 				pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
1066 				if (pd == null || pd.getWriteMethod() == null) {
1067 					if (pv.isOptional()) {
1068 						logger.debug("Ignoring optional value for property '" + actualName +
1069 								"' - property not found on bean class [" + getRootClass().getName() + "]");
1070 						return;
1071 					}
1072 					else {
1073 						PropertyMatches matches = PropertyMatches.forProperty(propertyName, getRootClass());
1074 						throw new NotWritablePropertyException(
1075 								getRootClass(), this.nestedPath + propertyName,
1076 								matches.buildErrorMessage(), matches.getPossibleMatches());
1077 					}
1078 				}
1079 				pv.getOriginalPropertyValue().resolvedDescriptor = pd;
1080 			}
1081 
1082 			Object oldValue = null;
1083 			try {
1084 				Object originalValue = pv.getValue();
1085 				Object valueToApply = originalValue;
1086 				if (!Boolean.FALSE.equals(pv.conversionNecessary)) {
1087 					if (pv.isConverted()) {
1088 						valueToApply = pv.getConvertedValue();
1089 					}
1090 					else {
1091 						if (isExtractOldValueForEditor() && pd.getReadMethod() != null) {
1092 							final Method readMethod = pd.getReadMethod();
1093 							if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers()) &&
1094 									!readMethod.isAccessible()) {
1095 								if (System.getSecurityManager()!= null) {
1096 									AccessController.doPrivileged(new PrivilegedAction<Object>() {
1097 										@Override
1098 										public Object run() {
1099 											readMethod.setAccessible(true);
1100 											return null;
1101 										}
1102 									});
1103 								}
1104 								else {
1105 									readMethod.setAccessible(true);
1106 								}
1107 							}
1108 							try {
1109 								if (System.getSecurityManager() != null) {
1110 									oldValue = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
1111 										@Override
1112 										public Object run() throws Exception {
1113 											return readMethod.invoke(object);
1114 										}
1115 									}, acc);
1116 								}
1117 								else {
1118 									oldValue = readMethod.invoke(object);
1119 								}
1120 							}
1121 							catch (Exception ex) {
1122 								if (ex instanceof PrivilegedActionException) {
1123 									ex = ((PrivilegedActionException) ex).getException();
1124 								}
1125 								if (logger.isDebugEnabled()) {
1126 									logger.debug("Could not read previous value of property '" +
1127 											this.nestedPath + propertyName + "'", ex);
1128 								}
1129 							}
1130 						}
1131 						valueToApply = convertForProperty(
1132 								propertyName, oldValue, originalValue, new TypeDescriptor(property(pd)));
1133 					}
1134 					pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue);
1135 				}
1136 				final Method writeMethod = (pd instanceof GenericTypeAwarePropertyDescriptor ?
1137 						((GenericTypeAwarePropertyDescriptor) pd).getWriteMethodForActualAccess() :
1138 						pd.getWriteMethod());
1139 				if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers()) && !writeMethod.isAccessible()) {
1140 					if (System.getSecurityManager()!= null) {
1141 						AccessController.doPrivileged(new PrivilegedAction<Object>() {
1142 							@Override
1143 							public Object run() {
1144 								writeMethod.setAccessible(true);
1145 								return null;
1146 							}
1147 						});
1148 					}
1149 					else {
1150 						writeMethod.setAccessible(true);
1151 					}
1152 				}
1153 				final Object value = valueToApply;
1154 				if (System.getSecurityManager() != null) {
1155 					try {
1156 						AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
1157 							@Override
1158 							public Object run() throws Exception {
1159 								writeMethod.invoke(object, value);
1160 								return null;
1161 							}
1162 						}, acc);
1163 					}
1164 					catch (PrivilegedActionException ex) {
1165 						throw ex.getException();
1166 					}
1167 				}
1168 				else {
1169 					writeMethod.invoke(this.object, value);
1170 				}
1171 			}
1172 			catch (TypeMismatchException ex) {
1173 				throw ex;
1174 			}
1175 			catch (InvocationTargetException ex) {
1176 				PropertyChangeEvent propertyChangeEvent =
1177 						new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue());
1178 				if (ex.getTargetException() instanceof ClassCastException) {
1179 					throw new TypeMismatchException(propertyChangeEvent, pd.getPropertyType(), ex.getTargetException());
1180 				}
1181 				else {
1182 					throw new MethodInvocationException(propertyChangeEvent, ex.getTargetException());
1183 				}
1184 			}
1185 			catch (Exception ex) {
1186 				PropertyChangeEvent pce =
1187 						new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, pv.getValue());
1188 				throw new MethodInvocationException(pce, ex);
1189 			}
1190 		}
1191 	}
1192 
1193 
1194 	@Override
1195 	public String toString() {
1196 		StringBuilder sb = new StringBuilder(getClass().getName());
1197 		if (this.object != null) {
1198 			sb.append(": wrapping object [").append(ObjectUtils.identityToString(this.object)).append("]");
1199 		}
1200 		else {
1201 			sb.append(": no wrapped object set");
1202 		}
1203 		return sb.toString();
1204 	}
1205 
1206 
1207 	private static class PropertyTokenHolder {
1208 
1209 		public String canonicalName;
1210 
1211 		public String actualName;
1212 
1213 		public String[] keys;
1214 	}
1215 
1216 
1217 	/**
1218 	 * Inner class to avoid a hard dependency on Java 8.
1219 	 */
1220 	@UsesJava8
1221 	private static class OptionalUnwrapper {
1222 
1223 		public static Object unwrap(Object optionalObject) {
1224 			Optional<?> optional = (Optional<?>) optionalObject;
1225 			Assert.isTrue(optional.isPresent(), "Optional value must be present");
1226 			Object result = optional.get();
1227 			Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");
1228 			return result;
1229 		}
1230 
1231 		public static boolean isEmpty(Object optionalObject) {
1232 			return !((Optional<?>) optionalObject).isPresent();
1233 		}
1234 	}
1235 
1236 }