001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.beans;
018:
019: import java.beans.PropertyChangeEvent;
020: import java.beans.PropertyDescriptor;
021: import java.lang.reflect.Array;
022: import java.lang.reflect.InvocationTargetException;
023: import java.lang.reflect.Method;
024: import java.lang.reflect.Modifier;
025: import java.util.ArrayList;
026: import java.util.HashMap;
027: import java.util.Iterator;
028: import java.util.List;
029: import java.util.Map;
030: import java.util.Set;
031:
032: import org.apache.commons.logging.Log;
033: import org.apache.commons.logging.LogFactory;
034:
035: import org.springframework.core.GenericCollectionTypeResolver;
036: import org.springframework.core.JdkVersion;
037: import org.springframework.core.MethodParameter;
038: import org.springframework.util.Assert;
039: import org.springframework.util.ObjectUtils;
040: import org.springframework.util.StringUtils;
041:
042: /**
043: * Default {@link BeanWrapper} implementation that should be sufficient
044: * for all typical use cases. Caches introspection results for efficiency.
045: *
046: * <p>Note: Auto-registers default property editors from the
047: * <code>org.springframework.beans.propertyeditors</code> package, which apply
048: * in addition to the JDK's standard PropertyEditors. Applications can call
049: * the {@link #registerCustomEditor(Class, java.beans.PropertyEditor)} method
050: * to register an editor for a particular instance (i.e. they are not shared
051: * across the application). See the base class
052: * {@link PropertyEditorRegistrySupport} for details.
053: *
054: * <p><code>BeanWrapperImpl</code> will convert collection and array values
055: * to the corresponding target collections or arrays, if necessary. Custom
056: * property editors that deal with collections or arrays can either be
057: * written via PropertyEditor's <code>setValue</code>, or against a
058: * comma-delimited String via <code>setAsText</code>, as String arrays are
059: * converted in such a format if the array itself is not assignable.
060: *
061: * @author Rod Johnson
062: * @author Juergen Hoeller
063: * @author Rob Harrop
064: * @since 15 April 2001
065: * @see #registerCustomEditor
066: * @see #setPropertyValues
067: * @see #setPropertyValue
068: * @see #getPropertyValue
069: * @see #getPropertyType
070: * @see BeanWrapper
071: * @see PropertyEditorRegistrySupport
072: */
073: public class BeanWrapperImpl extends AbstractPropertyAccessor implements
074: BeanWrapper {
075:
076: /**
077: * We'll create a lot of these objects, so we don't want a new logger every time.
078: */
079: private static final Log logger = LogFactory
080: .getLog(BeanWrapperImpl.class);
081:
082: /** The wrapped object */
083: private Object object;
084:
085: private String nestedPath = "";
086:
087: private Object rootObject;
088:
089: private TypeConverterDelegate typeConverterDelegate;
090:
091: /**
092: * Cached introspections results for this object, to prevent encountering
093: * the cost of JavaBeans introspection every time.
094: */
095: private CachedIntrospectionResults cachedIntrospectionResults;
096:
097: /**
098: * Map with cached nested BeanWrappers: nested path -> BeanWrapper instance.
099: */
100: private Map nestedBeanWrappers;
101:
102: /**
103: * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards.
104: * Registers default editors.
105: * @see #setWrappedInstance
106: */
107: public BeanWrapperImpl() {
108: this (true);
109: }
110:
111: /**
112: * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards.
113: * @param registerDefaultEditors whether to register default editors
114: * (can be suppressed if the BeanWrapper won't need any type conversion)
115: * @see #setWrappedInstance
116: */
117: public BeanWrapperImpl(boolean registerDefaultEditors) {
118: if (registerDefaultEditors) {
119: registerDefaultEditors();
120: }
121: this .typeConverterDelegate = new TypeConverterDelegate(this );
122: }
123:
124: /**
125: * Create new BeanWrapperImpl for the given object.
126: * @param object object wrapped by this BeanWrapper
127: */
128: public BeanWrapperImpl(Object object) {
129: registerDefaultEditors();
130: setWrappedInstance(object);
131: }
132:
133: /**
134: * Create new BeanWrapperImpl, wrapping a new instance of the specified class.
135: * @param clazz class to instantiate and wrap
136: */
137: public BeanWrapperImpl(Class clazz) {
138: registerDefaultEditors();
139: setWrappedInstance(BeanUtils.instantiateClass(clazz));
140: }
141:
142: /**
143: * Create new BeanWrapperImpl for the given object,
144: * registering a nested path that the object is in.
145: * @param object object wrapped by this BeanWrapper
146: * @param nestedPath the nested path of the object
147: * @param rootObject the root object at the top of the path
148: */
149: public BeanWrapperImpl(Object object, String nestedPath,
150: Object rootObject) {
151: registerDefaultEditors();
152: setWrappedInstance(object, nestedPath, rootObject);
153: }
154:
155: /**
156: * Create new BeanWrapperImpl for the given object,
157: * registering a nested path that the object is in.
158: * @param object object wrapped by this BeanWrapper
159: * @param nestedPath the nested path of the object
160: * @param superBw the containing BeanWrapper (must not be <code>null</code>)
161: */
162: private BeanWrapperImpl(Object object, String nestedPath,
163: BeanWrapperImpl super Bw) {
164: setWrappedInstance(object, nestedPath, super Bw
165: .getWrappedInstance());
166: setExtractOldValueForEditor(super Bw
167: .isExtractOldValueForEditor());
168: }
169:
170: //---------------------------------------------------------------------
171: // Implementation of BeanWrapper interface
172: //---------------------------------------------------------------------
173:
174: /**
175: * Switch the target object, replacing the cached introspection results only
176: * if the class of the new object is different to that of the replaced object.
177: * @param object the new target object
178: */
179: public void setWrappedInstance(Object object) {
180: setWrappedInstance(object, "", null);
181: }
182:
183: /**
184: * Switch the target object, replacing the cached introspection results only
185: * if the class of the new object is different to that of the replaced object.
186: * @param object the new target object
187: * @param nestedPath the nested path of the object
188: * @param rootObject the root object at the top of the path
189: */
190: public void setWrappedInstance(Object object, String nestedPath,
191: Object rootObject) {
192: Assert.notNull(object, "Bean object must not be null");
193: this .object = object;
194: this .nestedPath = (nestedPath != null ? nestedPath : "");
195: this .rootObject = (!"".equals(this .nestedPath) ? rootObject
196: : object);
197: this .nestedBeanWrappers = null;
198: this .typeConverterDelegate = new TypeConverterDelegate(this ,
199: object);
200: setIntrospectionClass(object.getClass());
201: }
202:
203: public final Object getWrappedInstance() {
204: return this .object;
205: }
206:
207: public final Class getWrappedClass() {
208: return (this .object != null ? this .object.getClass() : null);
209: }
210:
211: /**
212: * Return the nested path of the object wrapped by this BeanWrapper.
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 BeanWrapper.
220: * @see #getNestedPath
221: */
222: public final Object getRootInstance() {
223: return this .rootObject;
224: }
225:
226: /**
227: * Return the class of the root object at the top of the path of this BeanWrapper.
228: * @see #getNestedPath
229: */
230: public final Class getRootClass() {
231: return (this .rootObject != null ? this .rootObject.getClass()
232: : null);
233: }
234:
235: /**
236: * Set the class to introspect.
237: * Needs to be called when the target object changes.
238: * @param clazz the class to introspect
239: */
240: protected void setIntrospectionClass(Class clazz) {
241: if (this .cachedIntrospectionResults == null
242: || !this .cachedIntrospectionResults.getBeanClass()
243: .equals(clazz)) {
244: this .cachedIntrospectionResults = CachedIntrospectionResults
245: .forClass(clazz);
246: }
247: }
248:
249: public PropertyDescriptor[] getPropertyDescriptors() {
250: Assert.state(this .cachedIntrospectionResults != null,
251: "BeanWrapper does not hold a bean instance");
252: return this .cachedIntrospectionResults.getBeanInfo()
253: .getPropertyDescriptors();
254: }
255:
256: public PropertyDescriptor getPropertyDescriptor(String propertyName)
257: throws BeansException {
258: PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
259: if (pd == null) {
260: throw new InvalidPropertyException(getRootClass(),
261: this .nestedPath + propertyName, "No property '"
262: + propertyName + "' found");
263: }
264: return pd;
265: }
266:
267: /**
268: * Internal version of {@link #getPropertyDescriptor}:
269: * Returns <code>null</code> if not found rather than throwing an exception.
270: * @param propertyName the property to obtain the descriptor for
271: * @return the property descriptor for the specified property,
272: * or <code>null</code> if not found
273: * @throws BeansException in case of introspection failure
274: */
275: protected PropertyDescriptor getPropertyDescriptorInternal(
276: String propertyName) throws BeansException {
277: Assert.state(this .cachedIntrospectionResults != null,
278: "BeanWrapper does not hold a bean instance");
279: Assert.notNull(propertyName, "Property name must not be null");
280: BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
281: return nestedBw.cachedIntrospectionResults
282: .getPropertyDescriptor(getFinalPath(nestedBw,
283: propertyName));
284: }
285:
286: public Class getPropertyType(String propertyName)
287: throws BeansException {
288: try {
289: PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
290: if (pd != null) {
291: return pd.getPropertyType();
292: } else {
293: // Maybe an indexed/mapped property...
294: Object value = getPropertyValue(propertyName);
295: if (value != null) {
296: return value.getClass();
297: }
298: // Check to see if there is a custom editor,
299: // which might give an indication on the desired target type.
300: Class editorType = guessPropertyTypeFromEditors(propertyName);
301: if (editorType != null) {
302: return editorType;
303: }
304: }
305: } catch (InvalidPropertyException ex) {
306: // Consider as not determinable.
307: }
308: return null;
309: }
310:
311: public boolean isReadableProperty(String propertyName) {
312: try {
313: PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
314: if (pd != null) {
315: if (pd.getReadMethod() != null) {
316: return true;
317: }
318: } else {
319: // Maybe an indexed/mapped property...
320: getPropertyValue(propertyName);
321: return true;
322: }
323: } catch (InvalidPropertyException ex) {
324: // Cannot be evaluated, so can't be readable.
325: }
326: return false;
327: }
328:
329: public boolean isWritableProperty(String propertyName) {
330: try {
331: PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
332: if (pd != null) {
333: if (pd.getWriteMethod() != null) {
334: return true;
335: }
336: } else {
337: // Maybe an indexed/mapped property...
338: getPropertyValue(propertyName);
339: return true;
340: }
341: } catch (InvalidPropertyException ex) {
342: // Cannot be evaluated, so can't be writable.
343: }
344: return false;
345: }
346:
347: //---------------------------------------------------------------------
348: // Implementation of TypeConverter interface
349: //---------------------------------------------------------------------
350:
351: /**
352: * @deprecated in favor of <code>convertIfNecessary</code>
353: * @see #convertIfNecessary(Object, Class)
354: */
355: public Object doTypeConversionIfNecessary(Object value,
356: Class requiredType) throws TypeMismatchException {
357: return convertIfNecessary(value, requiredType, null);
358: }
359:
360: public Object convertIfNecessary(Object value, Class requiredType)
361: throws TypeMismatchException {
362: return convertIfNecessary(value, requiredType, null);
363: }
364:
365: public Object convertIfNecessary(Object value, Class requiredType,
366: MethodParameter methodParam) throws TypeMismatchException {
367: try {
368: return this .typeConverterDelegate.convertIfNecessary(value,
369: requiredType, methodParam);
370: } catch (IllegalArgumentException ex) {
371: throw new TypeMismatchException(value, requiredType, ex);
372: }
373: }
374:
375: /**
376: * Convert the given value for the specified property to the latter's type.
377: * <p>This method is only intended for optimizations in a BeanFactory.
378: * Use the <code>convertIfNecessary</code> methods for programmatic conversion.
379: * @param value the value to convert
380: * @param propertyName the target property
381: * (note that nested or indexed properties are not supported here)
382: * @return the new value, possibly the result of type conversion
383: * @throws TypeMismatchException if type conversion failed
384: */
385: public Object convertForProperty(Object value, String propertyName)
386: throws TypeMismatchException {
387: PropertyDescriptor pd = this .cachedIntrospectionResults
388: .getPropertyDescriptor(propertyName);
389: if (pd == null) {
390: throw new InvalidPropertyException(getRootClass(),
391: this .nestedPath + propertyName, "No property '"
392: + propertyName + "' found");
393: }
394: try {
395: return this .typeConverterDelegate.convertIfNecessary(null,
396: value, pd);
397: } catch (IllegalArgumentException ex) {
398: PropertyChangeEvent pce = new PropertyChangeEvent(
399: this .rootObject, this .nestedPath + propertyName,
400: null, value);
401: throw new TypeMismatchException(pce, pd.getPropertyType(),
402: ex);
403: }
404: }
405:
406: //---------------------------------------------------------------------
407: // Implementation methods
408: //---------------------------------------------------------------------
409:
410: /**
411: * Get the last component of the path. Also works if not nested.
412: * @param bw BeanWrapper to work on
413: * @param nestedPath property path we know is nested
414: * @return last component of the path (the property on the target bean)
415: */
416: private String getFinalPath(BeanWrapper bw, String nestedPath) {
417: if (bw == this ) {
418: return nestedPath;
419: }
420: return nestedPath.substring(PropertyAccessorUtils
421: .getLastNestedPropertySeparatorIndex(nestedPath) + 1);
422: }
423:
424: /**
425: * Recursively navigate to return a BeanWrapper for the nested property path.
426: * @param propertyPath property property path, which may be nested
427: * @return a BeanWrapper for the target bean
428: */
429: protected BeanWrapperImpl getBeanWrapperForPropertyPath(
430: String propertyPath) {
431: int pos = PropertyAccessorUtils
432: .getFirstNestedPropertySeparatorIndex(propertyPath);
433: // Handle nested properties recursively.
434: if (pos > -1) {
435: String nestedProperty = propertyPath.substring(0, pos);
436: String nestedPath = propertyPath.substring(pos + 1);
437: BeanWrapperImpl nestedBw = getNestedBeanWrapper(nestedProperty);
438: return nestedBw.getBeanWrapperForPropertyPath(nestedPath);
439: } else {
440: return this ;
441: }
442: }
443:
444: /**
445: * Retrieve a BeanWrapper for the given nested property.
446: * Create a new one if not found in the cache.
447: * <p>Note: Caching nested BeanWrappers is necessary now,
448: * to keep registered custom editors for nested properties.
449: * @param nestedProperty property to create the BeanWrapper for
450: * @return the BeanWrapper instance, either cached or newly created
451: */
452: private BeanWrapperImpl getNestedBeanWrapper(String nestedProperty) {
453: if (this .nestedBeanWrappers == null) {
454: this .nestedBeanWrappers = new HashMap();
455: }
456: // Get value of bean property.
457: PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty);
458: String canonicalName = tokens.canonicalName;
459: Object propertyValue = getPropertyValue(tokens);
460: if (propertyValue == null) {
461: throw new NullValueInNestedPathException(getRootClass(),
462: this .nestedPath + canonicalName);
463: }
464:
465: // Lookup cached sub-BeanWrapper, create new one if not found.
466: BeanWrapperImpl nestedBw = (BeanWrapperImpl) this .nestedBeanWrappers
467: .get(canonicalName);
468: if (nestedBw == null
469: || nestedBw.getWrappedInstance() != propertyValue) {
470: if (logger.isTraceEnabled()) {
471: logger
472: .trace("Creating new nested BeanWrapper for property '"
473: + canonicalName + "'");
474: }
475: nestedBw = newNestedBeanWrapper(propertyValue,
476: this .nestedPath + canonicalName
477: + NESTED_PROPERTY_SEPARATOR);
478: // Inherit all type-specific PropertyEditors.
479: copyDefaultEditorsTo(nestedBw);
480: copyCustomEditorsTo(nestedBw, canonicalName);
481: this .nestedBeanWrappers.put(canonicalName, nestedBw);
482: } else {
483: if (logger.isTraceEnabled()) {
484: logger
485: .trace("Using cached nested BeanWrapper for property '"
486: + canonicalName + "'");
487: }
488: }
489: return nestedBw;
490: }
491:
492: /**
493: * Create a new nested BeanWrapper instance.
494: * <p>Default implementation creates a BeanWrapperImpl instance.
495: * Can be overridden in subclasses to create a BeanWrapperImpl subclass.
496: * @param object object wrapped by this BeanWrapper
497: * @param nestedPath the nested path of the object
498: * @return the nested BeanWrapper instance
499: */
500: protected BeanWrapperImpl newNestedBeanWrapper(Object object,
501: String nestedPath) {
502: return new BeanWrapperImpl(object, nestedPath, this );
503: }
504:
505: /**
506: * Parse the given property name into the corresponding property name tokens.
507: * @param propertyName the property name to parse
508: * @return representation of the parsed property tokens
509: */
510: private PropertyTokenHolder getPropertyNameTokens(
511: String propertyName) {
512: PropertyTokenHolder tokens = new PropertyTokenHolder();
513: String actualName = null;
514: List keys = new ArrayList(2);
515: int searchIndex = 0;
516: while (searchIndex != -1) {
517: int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX,
518: searchIndex);
519: searchIndex = -1;
520: if (keyStart != -1) {
521: int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX,
522: keyStart + PROPERTY_KEY_PREFIX.length());
523: if (keyEnd != -1) {
524: if (actualName == null) {
525: actualName = propertyName
526: .substring(0, keyStart);
527: }
528: String key = propertyName.substring(keyStart
529: + PROPERTY_KEY_PREFIX.length(), keyEnd);
530: if ((key.startsWith("'") && key.endsWith("'"))
531: || (key.startsWith("\"") && key
532: .endsWith("\""))) {
533: key = key.substring(1, key.length() - 1);
534: }
535: keys.add(key);
536: searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length();
537: }
538: }
539: }
540: tokens.actualName = (actualName != null ? actualName
541: : propertyName);
542: tokens.canonicalName = tokens.actualName;
543: if (!keys.isEmpty()) {
544: tokens.canonicalName += PROPERTY_KEY_PREFIX
545: + StringUtils.collectionToDelimitedString(keys,
546: PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX)
547: + PROPERTY_KEY_SUFFIX;
548: tokens.keys = StringUtils.toStringArray(keys);
549: }
550: return tokens;
551: }
552:
553: //---------------------------------------------------------------------
554: // Implementation of PropertyAccessor interface
555: //---------------------------------------------------------------------
556:
557: public Object getPropertyValue(String propertyName)
558: throws BeansException {
559: BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
560: PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(
561: nestedBw, propertyName));
562: return nestedBw.getPropertyValue(tokens);
563: }
564:
565: private Object getPropertyValue(PropertyTokenHolder tokens)
566: throws BeansException {
567: String propertyName = tokens.canonicalName;
568: String actualName = tokens.actualName;
569: PropertyDescriptor pd = this .cachedIntrospectionResults
570: .getPropertyDescriptor(actualName);
571: if (pd == null || pd.getReadMethod() == null) {
572: throw new NotReadablePropertyException(getRootClass(),
573: this .nestedPath + propertyName);
574: }
575: Method readMethod = pd.getReadMethod();
576: try {
577: if (!Modifier.isPublic(readMethod.getDeclaringClass()
578: .getModifiers())) {
579: readMethod.setAccessible(true);
580: }
581: Object value = readMethod.invoke(this .object,
582: (Object[]) null);
583: if (tokens.keys != null) {
584: // apply indexes and map keys
585: for (int i = 0; i < tokens.keys.length; i++) {
586: String key = tokens.keys[i];
587: if (value == null) {
588: throw new NullValueInNestedPathException(
589: getRootClass(), this .nestedPath
590: + propertyName,
591: "Cannot access indexed value of property referenced in indexed "
592: + "property path '"
593: + propertyName
594: + "': returned null");
595: } else if (value.getClass().isArray()) {
596: value = Array.get(value, Integer.parseInt(key));
597: } else if (value instanceof List) {
598: List list = (List) value;
599: value = list.get(Integer.parseInt(key));
600: } else if (value instanceof Set) {
601: // Apply index to Iterator in case of a Set.
602: Set set = (Set) value;
603: int index = Integer.parseInt(key);
604: if (index < 0 || index >= set.size()) {
605: throw new InvalidPropertyException(
606: getRootClass(),
607: this .nestedPath + propertyName,
608: "Cannot get element with index "
609: + index
610: + " from Set of size "
611: + set.size()
612: + ", accessed using property path '"
613: + propertyName + "'");
614: }
615: Iterator it = set.iterator();
616: for (int j = 0; it.hasNext(); j++) {
617: Object elem = it.next();
618: if (j == index) {
619: value = elem;
620: break;
621: }
622: }
623: } else if (value instanceof Map) {
624: Map map = (Map) value;
625: Class mapKeyType = null;
626: if (JdkVersion.isAtLeastJava15()) {
627: mapKeyType = GenericCollectionTypeResolver
628: .getMapKeyReturnType(pd
629: .getReadMethod(), i + 1);
630: }
631: // IMPORTANT: Do not pass full property name in here - property editors
632: // must not kick in for map keys but rather only for map values.
633: Object convertedMapKey = this .typeConverterDelegate
634: .convertIfNecessary(key, mapKeyType);
635: // Pass full property name and old value in here, since we want full
636: // conversion ability for map values.
637: value = map.get(convertedMapKey);
638: } else {
639: throw new InvalidPropertyException(
640: getRootClass(),
641: this .nestedPath + propertyName,
642: "Property referenced in indexed property path '"
643: + propertyName
644: + "' is neither an array nor a List nor a Set nor a Map; returned value was ["
645: + value + "]");
646: }
647: }
648: }
649: return value;
650: } catch (InvocationTargetException ex) {
651: throw new InvalidPropertyException(getRootClass(),
652: this .nestedPath + propertyName,
653: "Getter for property '" + actualName
654: + "' threw exception", ex);
655: } catch (IllegalAccessException ex) {
656: throw new InvalidPropertyException(getRootClass(),
657: this .nestedPath + propertyName,
658: "Illegal attempt to get property '" + actualName
659: + "' threw exception", ex);
660: } catch (IndexOutOfBoundsException ex) {
661: throw new InvalidPropertyException(getRootClass(),
662: this .nestedPath + propertyName,
663: "Index of out of bounds in property path '"
664: + propertyName + "'", ex);
665: } catch (NumberFormatException ex) {
666: throw new InvalidPropertyException(getRootClass(),
667: this .nestedPath + propertyName,
668: "Invalid index in property path '" + propertyName
669: + "'", ex);
670: }
671: }
672:
673: public void setPropertyValue(String propertyName, Object value)
674: throws BeansException {
675: BeanWrapperImpl nestedBw = null;
676: try {
677: nestedBw = getBeanWrapperForPropertyPath(propertyName);
678: } catch (NotReadablePropertyException ex) {
679: throw new NotWritablePropertyException(getRootClass(),
680: this .nestedPath + propertyName,
681: "Nested property in path '" + propertyName
682: + "' does not exist", ex);
683: }
684: PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(
685: nestedBw, propertyName));
686: nestedBw.setPropertyValue(tokens, new PropertyValue(
687: propertyName, value));
688: }
689:
690: public void setPropertyValue(PropertyValue pv)
691: throws BeansException {
692: PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens;
693: if (tokens == null) {
694: String propertyName = pv.getName();
695: BeanWrapperImpl nestedBw = null;
696: try {
697: nestedBw = getBeanWrapperForPropertyPath(propertyName);
698: } catch (NotReadablePropertyException ex) {
699: throw new NotWritablePropertyException(getRootClass(),
700: this .nestedPath + propertyName,
701: "Nested property in path '" + propertyName
702: + "' does not exist", ex);
703: }
704: tokens = getPropertyNameTokens(getFinalPath(nestedBw,
705: propertyName));
706: if (nestedBw == this ) {
707: pv.resolvedTokens = tokens;
708: }
709: nestedBw.setPropertyValue(tokens, pv);
710: } else {
711: setPropertyValue(tokens, pv);
712: }
713: }
714:
715: private void setPropertyValue(PropertyTokenHolder tokens,
716: PropertyValue pv) throws BeansException {
717: String propertyName = tokens.canonicalName;
718: String actualName = tokens.actualName;
719:
720: if (tokens.keys != null) {
721: // Apply indexes and map keys: fetch value for all keys but the last one.
722: PropertyTokenHolder getterTokens = new PropertyTokenHolder();
723: getterTokens.canonicalName = tokens.canonicalName;
724: getterTokens.actualName = tokens.actualName;
725: getterTokens.keys = new String[tokens.keys.length - 1];
726: System.arraycopy(tokens.keys, 0, getterTokens.keys, 0,
727: tokens.keys.length - 1);
728: Object propValue = null;
729: try {
730: propValue = getPropertyValue(getterTokens);
731: } catch (NotReadablePropertyException ex) {
732: throw new NotWritablePropertyException(getRootClass(),
733: this .nestedPath + propertyName,
734: "Cannot access indexed value in property referenced "
735: + "in indexed property path '"
736: + propertyName + "'", ex);
737: }
738: // Set value for last key.
739: String key = tokens.keys[tokens.keys.length - 1];
740: if (propValue == null) {
741: throw new NullValueInNestedPathException(
742: getRootClass(), this .nestedPath + propertyName,
743: "Cannot access indexed value in property referenced "
744: + "in indexed property path '"
745: + propertyName + "': returned null");
746: } else if (propValue.getClass().isArray()) {
747: Class requiredType = propValue.getClass()
748: .getComponentType();
749: int arrayIndex = Integer.parseInt(key);
750: Object oldValue = null;
751: try {
752: if (isExtractOldValueForEditor()) {
753: oldValue = Array.get(propValue, arrayIndex);
754: }
755: Object convertedValue = this .typeConverterDelegate
756: .convertIfNecessary(propertyName, oldValue,
757: pv.getValue(), requiredType);
758: Array.set(propValue, Integer.parseInt(key),
759: convertedValue);
760: } catch (IllegalArgumentException ex) {
761: PropertyChangeEvent pce = new PropertyChangeEvent(
762: this .rootObject, this .nestedPath
763: + propertyName, oldValue, pv
764: .getValue());
765: throw new TypeMismatchException(pce, requiredType,
766: ex);
767: } catch (IndexOutOfBoundsException ex) {
768: throw new InvalidPropertyException(getRootClass(),
769: this .nestedPath + propertyName,
770: "Invalid array index in property path '"
771: + propertyName + "'", ex);
772: }
773: } else if (propValue instanceof List) {
774: PropertyDescriptor pd = this .cachedIntrospectionResults
775: .getPropertyDescriptor(actualName);
776: Class requiredType = null;
777: if (JdkVersion.isAtLeastJava15()) {
778: requiredType = GenericCollectionTypeResolver
779: .getCollectionReturnType(
780: pd.getReadMethod(),
781: tokens.keys.length);
782: }
783: List list = (List) propValue;
784: int index = Integer.parseInt(key);
785: Object oldValue = null;
786: if (isExtractOldValueForEditor() && index < list.size()) {
787: oldValue = list.get(index);
788: }
789: try {
790: Object convertedValue = this .typeConverterDelegate
791: .convertIfNecessary(propertyName, oldValue,
792: pv.getValue(), requiredType);
793: if (index < list.size()) {
794: list.set(index, convertedValue);
795: } else if (index >= list.size()) {
796: for (int i = list.size(); i < index; i++) {
797: try {
798: list.add(null);
799: } catch (NullPointerException ex) {
800: throw new InvalidPropertyException(
801: getRootClass(),
802: this .nestedPath + propertyName,
803: "Cannot set element with index "
804: + index
805: + " in List of size "
806: + list.size()
807: + ", accessed using property path '"
808: + propertyName
809: + "': List does not support filling up gaps with null elements");
810: }
811: }
812: list.add(convertedValue);
813: }
814: } catch (IllegalArgumentException ex) {
815: PropertyChangeEvent pce = new PropertyChangeEvent(
816: this .rootObject, this .nestedPath
817: + propertyName, oldValue, pv
818: .getValue());
819: throw new TypeMismatchException(pce, requiredType,
820: ex);
821: }
822: } else if (propValue instanceof Map) {
823: PropertyDescriptor pd = this .cachedIntrospectionResults
824: .getPropertyDescriptor(actualName);
825: Class mapKeyType = null;
826: Class mapValueType = null;
827: if (JdkVersion.isAtLeastJava15()) {
828: mapKeyType = GenericCollectionTypeResolver
829: .getMapKeyReturnType(pd.getReadMethod(),
830: tokens.keys.length);
831: mapValueType = GenericCollectionTypeResolver
832: .getMapValueReturnType(pd.getReadMethod(),
833: tokens.keys.length);
834: }
835: Map map = (Map) propValue;
836: Object oldValue = null;
837: if (isExtractOldValueForEditor()) {
838: oldValue = map.get(key);
839: }
840: Object convertedMapKey = null;
841: Object convertedMapValue = null;
842: try {
843: // IMPORTANT: Do not pass full property name in here - property editors
844: // must not kick in for map keys but rather only for map values.
845: convertedMapKey = this .typeConverterDelegate
846: .convertIfNecessary(key, mapKeyType);
847: } catch (IllegalArgumentException ex) {
848: PropertyChangeEvent pce = new PropertyChangeEvent(
849: this .rootObject, this .nestedPath
850: + propertyName, oldValue, pv
851: .getValue());
852: throw new TypeMismatchException(pce, mapKeyType, ex);
853: }
854: try {
855: // Pass full property name and old value in here, since we want full
856: // conversion ability for map values.
857: convertedMapValue = this .typeConverterDelegate
858: .convertIfNecessary(propertyName, oldValue,
859: pv.getValue(), mapValueType, null,
860: new MethodParameter(pd
861: .getReadMethod(), -1,
862: tokens.keys.length + 1));
863: } catch (IllegalArgumentException ex) {
864: PropertyChangeEvent pce = new PropertyChangeEvent(
865: this .rootObject, this .nestedPath
866: + propertyName, oldValue, pv
867: .getValue());
868: throw new TypeMismatchException(pce, mapValueType,
869: ex);
870: }
871: map.put(convertedMapKey, convertedMapValue);
872: } else {
873: throw new InvalidPropertyException(
874: getRootClass(),
875: this .nestedPath + propertyName,
876: "Property referenced in indexed property path '"
877: + propertyName
878: + "' is neither an array nor a List nor a Map; returned value was ["
879: + pv.getValue() + "]");
880: }
881: }
882:
883: else {
884: PropertyDescriptor pd = this .cachedIntrospectionResults
885: .getPropertyDescriptor(actualName);
886: if (pd == null || pd.getWriteMethod() == null) {
887: PropertyMatches matches = PropertyMatches.forProperty(
888: propertyName, getRootClass());
889: throw new NotWritablePropertyException(getRootClass(),
890: this .nestedPath + propertyName, matches
891: .buildErrorMessage(), matches
892: .getPossibleMatches());
893: }
894:
895: Object oldValue = null;
896: if (isExtractOldValueForEditor()
897: && pd.getReadMethod() != null) {
898: Method readMethod = pd.getReadMethod();
899: if (!Modifier.isPublic(readMethod.getDeclaringClass()
900: .getModifiers())) {
901: readMethod.setAccessible(true);
902: }
903: try {
904: oldValue = readMethod.invoke(this .object,
905: new Object[0]);
906: } catch (Exception ex) {
907: if (logger.isDebugEnabled()) {
908: logger.debug(
909: "Could not read previous value of property '"
910: + this .nestedPath
911: + propertyName + "'", ex);
912: }
913: }
914: }
915:
916: try {
917: Object convertedValue = (pv.isConverted() ? pv
918: .getConvertedValue()
919: : this .typeConverterDelegate
920: .convertIfNecessary(oldValue, pv
921: .getValue(), pd));
922: Method writeMethod = pd.getWriteMethod();
923: if (!Modifier.isPublic(writeMethod.getDeclaringClass()
924: .getModifiers())) {
925: writeMethod.setAccessible(true);
926: }
927: writeMethod.invoke(this .object,
928: new Object[] { convertedValue });
929: } catch (InvocationTargetException ex) {
930: PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(
931: this .rootObject,
932: this .nestedPath + propertyName, oldValue, pv
933: .getValue());
934: if (ex.getTargetException() instanceof ClassCastException) {
935: throw new TypeMismatchException(
936: propertyChangeEvent, pd.getPropertyType(),
937: ex.getTargetException());
938: } else {
939: throw new MethodInvocationException(
940: propertyChangeEvent, ex
941: .getTargetException());
942: }
943: } catch (IllegalArgumentException ex) {
944: PropertyChangeEvent pce = new PropertyChangeEvent(
945: this .rootObject,
946: this .nestedPath + propertyName, oldValue, pv
947: .getValue());
948: throw new TypeMismatchException(pce, pd
949: .getPropertyType(), ex);
950: } catch (IllegalAccessException ex) {
951: PropertyChangeEvent pce = new PropertyChangeEvent(
952: this .rootObject,
953: this .nestedPath + propertyName, oldValue, pv
954: .getValue());
955: throw new MethodInvocationException(pce, ex);
956: }
957: }
958: }
959:
960: public String toString() {
961: StringBuffer sb = new StringBuffer(getClass().getName());
962: if (this .object != null) {
963: sb.append(": wrapping object [").append(
964: ObjectUtils.identityToString(this .object)).append(
965: "]");
966: } else {
967: sb.append(": no wrapped object set");
968: }
969: return sb.toString();
970: }
971:
972: //---------------------------------------------------------------------
973: // Inner class for internal use
974: //---------------------------------------------------------------------
975:
976: private static class PropertyTokenHolder {
977:
978: public String canonicalName;
979:
980: public String actualName;
981:
982: public String[] keys;
983: }
984:
985: }
|