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.PropertyDescriptor;
020: import java.beans.PropertyEditor;
021: import java.beans.PropertyEditorManager;
022: import java.lang.reflect.Array;
023: import java.lang.reflect.Field;
024: import java.util.Collection;
025: import java.util.Collections;
026: import java.util.Iterator;
027: import java.util.Map;
028: import java.util.WeakHashMap;
029:
030: import org.apache.commons.logging.Log;
031: import org.apache.commons.logging.LogFactory;
032:
033: import org.springframework.core.CollectionFactory;
034: import org.springframework.core.GenericCollectionTypeResolver;
035: import org.springframework.core.JdkVersion;
036: import org.springframework.core.MethodParameter;
037: import org.springframework.util.ClassUtils;
038: import org.springframework.util.StringUtils;
039:
040: /**
041: * Internal helper class for converting property values to target types.
042: *
043: * <p>Works on a given {@link PropertyEditorRegistrySupport} instance.
044: * Used as a delegate by {@link BeanWrapperImpl} and {@link SimpleTypeConverter}.
045: *
046: * @author Juergen Hoeller
047: * @author Rob Harrop
048: * @since 2.0
049: * @see BeanWrapperImpl
050: * @see SimpleTypeConverter
051: */
052: class TypeConverterDelegate {
053:
054: private static final Log logger = LogFactory
055: .getLog(TypeConverterDelegate.class);
056:
057: private static final Map unknownEditorTypes = Collections
058: .synchronizedMap(new WeakHashMap());
059:
060: private final PropertyEditorRegistrySupport propertyEditorRegistry;
061:
062: private final Object targetObject;
063:
064: /**
065: * Create a new TypeConverterDelegate for the given editor registry.
066: * @param propertyEditorRegistry the editor registry to use
067: */
068: public TypeConverterDelegate(
069: PropertyEditorRegistrySupport propertyEditorRegistry) {
070: this (propertyEditorRegistry, null);
071: }
072:
073: /**
074: * Create a new TypeConverterDelegate for the given editor registry and bean instance.
075: * @param propertyEditorRegistry the editor registry to use
076: * @param targetObject the target object to work on (as context that can be passed to editors)
077: */
078: public TypeConverterDelegate(
079: PropertyEditorRegistrySupport propertyEditorRegistry,
080: Object targetObject) {
081: this .propertyEditorRegistry = propertyEditorRegistry;
082: this .targetObject = targetObject;
083: }
084:
085: /**
086: * Convert the value to the specified required type.
087: * @param newValue the proposed new value
088: * @param requiredType the type we must convert to
089: * (or <code>null</code> if not known, for example in case of a collection element)
090: * @return the new value, possibly the result of type conversion
091: * @throws IllegalArgumentException if type conversion failed
092: */
093: public Object convertIfNecessary(Object newValue, Class requiredType)
094: throws IllegalArgumentException {
095: return convertIfNecessary(null, null, newValue, requiredType,
096: null, null);
097: }
098:
099: /**
100: * Convert the value to the specified required type.
101: * @param newValue the proposed new value
102: * @param requiredType the type we must convert to
103: * (or <code>null</code> if not known, for example in case of a collection element)
104: * @param methodParam the method parameter that is the target of the conversion
105: * (may be <code>null</code>)
106: * @return the new value, possibly the result of type conversion
107: * @throws IllegalArgumentException if type conversion failed
108: */
109: public Object convertIfNecessary(Object newValue,
110: Class requiredType, MethodParameter methodParam)
111: throws IllegalArgumentException {
112:
113: return convertIfNecessary(null, null, newValue, requiredType,
114: null, methodParam);
115: }
116:
117: /**
118: * Convert the value to the required type for the specified property.
119: * @param propertyName name of the property
120: * @param oldValue the previous value, if available (may be <code>null</code>)
121: * @param newValue the proposed new value
122: * @param requiredType the type we must convert to
123: * (or <code>null</code> if not known, for example in case of a collection element)
124: * @return the new value, possibly the result of type conversion
125: * @throws IllegalArgumentException if type conversion failed
126: */
127: public Object convertIfNecessary(String propertyName,
128: Object oldValue, Object newValue, Class requiredType)
129: throws IllegalArgumentException {
130:
131: return convertIfNecessary(propertyName, oldValue, newValue,
132: requiredType, null, null);
133: }
134:
135: /**
136: * Convert the value to the required type for the specified property.
137: * @param oldValue the previous value, if available (may be <code>null</code>)
138: * @param newValue the proposed new value
139: * @param descriptor the JavaBeans descriptor for the property
140: * @return the new value, possibly the result of type conversion
141: * @throws IllegalArgumentException if type conversion failed
142: */
143: public Object convertIfNecessary(Object oldValue, Object newValue,
144: PropertyDescriptor descriptor)
145: throws IllegalArgumentException {
146:
147: return convertIfNecessary(descriptor.getName(), oldValue,
148: newValue, descriptor.getPropertyType(), descriptor,
149: new MethodParameter(descriptor.getWriteMethod(), 0));
150: }
151:
152: /**
153: * Convert the value to the required type (if necessary from a String),
154: * for the specified property.
155: * @param propertyName name of the property
156: * @param oldValue the previous value, if available (may be <code>null</code>)
157: * @param newValue the proposed new value
158: * @param requiredType the type we must convert to
159: * (or <code>null</code> if not known, for example in case of a collection element)
160: * @param descriptor the JavaBeans descriptor for the property
161: * @param methodParam the method parameter that is the target of the conversion
162: * (may be <code>null</code>)
163: * @return the new value, possibly the result of type conversion
164: * @throws IllegalArgumentException if type conversion failed
165: */
166: protected Object convertIfNecessary(String propertyName,
167: Object oldValue, Object newValue, Class requiredType,
168: PropertyDescriptor descriptor, MethodParameter methodParam)
169: throws IllegalArgumentException {
170:
171: Object convertedValue = newValue;
172:
173: // Custom editor for this type?
174: PropertyEditor editor = this .propertyEditorRegistry
175: .findCustomEditor(requiredType, propertyName);
176:
177: // Value not of required type?
178: if (editor != null
179: || (requiredType != null && !ClassUtils
180: .isAssignableValue(requiredType, convertedValue))) {
181: if (editor == null && descriptor != null) {
182: if (JdkVersion.isAtLeastJava15()) {
183: editor = descriptor
184: .createPropertyEditor(this .targetObject);
185: } else {
186: Class editorClass = descriptor
187: .getPropertyEditorClass();
188: if (editorClass != null) {
189: editor = (PropertyEditor) BeanUtils
190: .instantiateClass(editorClass);
191: }
192: }
193: }
194: if (editor == null && requiredType != null) {
195: // No custom editor -> check BeanWrapperImpl's default editors.
196: editor = (PropertyEditor) this .propertyEditorRegistry
197: .getDefaultEditor(requiredType);
198: if (editor == null
199: && !unknownEditorTypes
200: .containsKey(requiredType)) {
201: // No BeanWrapper default editor -> check standard JavaBean editors.
202: editor = PropertyEditorManager
203: .findEditor(requiredType);
204: if (editor == null) {
205: unknownEditorTypes.put(requiredType,
206: Boolean.TRUE);
207: }
208: }
209: }
210: convertedValue = doConvertValue(oldValue, convertedValue,
211: requiredType, editor);
212: }
213:
214: if (requiredType != null) {
215: // Try to apply some standard type conversion rules if appropriate.
216:
217: if (convertedValue != null) {
218: if (String.class.equals(requiredType)
219: && ClassUtils
220: .isPrimitiveOrWrapper(convertedValue
221: .getClass())) {
222: // We can stringify any primitive value...
223: return convertedValue.toString();
224: } else if (requiredType.isArray()) {
225: // Array required -> apply appropriate conversion of elements.
226: return convertToTypedArray(convertedValue,
227: propertyName, requiredType
228: .getComponentType());
229: } else if (convertedValue instanceof Collection
230: && CollectionFactory
231: .isApproximableCollectionType(requiredType)) {
232: // Convert elements to target type, if determined.
233: convertedValue = convertToTypedCollection(
234: (Collection) convertedValue, propertyName,
235: methodParam);
236: } else if (convertedValue instanceof Map
237: && CollectionFactory
238: .isApproximableMapType(requiredType)) {
239: // Convert keys and values to respective target type, if determined.
240: convertedValue = convertToTypedMap(
241: (Map) convertedValue, propertyName,
242: methodParam);
243: } else if (convertedValue instanceof String
244: && !requiredType.isInstance(convertedValue)) {
245: if (JdkVersion.isAtLeastJava15()
246: && requiredType.isEnum()
247: && "".equals(convertedValue)) {
248: // It's an empty enum identifier: reset the enum value to null.
249: return null;
250: }
251: // Try field lookup as fallback: for JDK 1.5 enum or custom enum
252: // with values defined as static fields. Resulting value still needs
253: // to be checked, hence we don't return it right away.
254: try {
255: Field enumField = requiredType
256: .getField((String) convertedValue);
257: convertedValue = enumField.get(null);
258: } catch (Throwable ex) {
259: if (logger.isTraceEnabled()) {
260: logger.trace("Field [" + convertedValue
261: + "] isn't an enum value", ex);
262: }
263: }
264: }
265: }
266:
267: if (!ClassUtils.isAssignableValue(requiredType,
268: convertedValue)) {
269: // Definitely doesn't match: throw IllegalArgumentException.
270: throw new IllegalArgumentException(
271: "Cannot convert value of type ["
272: + (newValue != null ? ClassUtils
273: .getQualifiedName(newValue
274: .getClass()) : null)
275: + "] to required type ["
276: + ClassUtils
277: .getQualifiedName(requiredType)
278: + "]"
279: + (propertyName != null ? " for property '"
280: + propertyName + "'"
281: : "")
282: + ": no matching editors or conversion strategy found");
283: }
284: }
285:
286: return convertedValue;
287: }
288:
289: /**
290: * Convert the value to the required type (if necessary from a String),
291: * using the given property editor.
292: * @param oldValue the previous value, if available (may be <code>null</code>)
293: * @param newValue the proposed new value
294: * @param requiredType the type we must convert to
295: * (or <code>null</code> if not known, for example in case of a collection element)
296: * @param editor the PropertyEditor to use
297: * @return the new value, possibly the result of type conversion
298: * @throws IllegalArgumentException if type conversion failed
299: */
300: protected Object doConvertValue(Object oldValue, Object newValue,
301: Class requiredType, PropertyEditor editor) {
302: Object convertedValue = newValue;
303: boolean sharedEditor = false;
304:
305: if (editor != null) {
306: sharedEditor = this .propertyEditorRegistry
307: .isSharedEditor(editor);
308: }
309:
310: if (editor != null && !(convertedValue instanceof String)) {
311: // Not a String -> use PropertyEditor's setValue.
312: // With standard PropertyEditors, this will return the very same object;
313: // we just want to allow special PropertyEditors to override setValue
314: // for type conversion from non-String values to the required type.
315: try {
316: Object newConvertedValue = null;
317: if (sharedEditor) {
318: // Synchronized access to shared editor instance.
319: synchronized (editor) {
320: editor.setValue(convertedValue);
321: newConvertedValue = editor.getValue();
322: }
323: } else {
324: // Unsynchronized access to non-shared editor instance.
325: editor.setValue(convertedValue);
326: newConvertedValue = editor.getValue();
327: }
328: if (newConvertedValue != convertedValue) {
329: convertedValue = newConvertedValue;
330: // Reset PropertyEditor: It already did a proper conversion.
331: // Don't use it again for a setAsText call.
332: editor = null;
333: }
334: } catch (Exception ex) {
335: if (logger.isDebugEnabled()) {
336: logger.debug("PropertyEditor ["
337: + editor.getClass().getName()
338: + "] does not support setValue call", ex);
339: }
340: // Swallow and proceed.
341: }
342: }
343:
344: if (requiredType != null && !requiredType.isArray()
345: && convertedValue instanceof String[]) {
346: // Convert String array to a comma-separated String.
347: // Only applies if no PropertyEditor converted the String array before.
348: // The CSV String will be passed into a PropertyEditor's setAsText method, if any.
349: if (logger.isTraceEnabled()) {
350: logger
351: .trace("Converting String array to comma-delimited String ["
352: + convertedValue + "]");
353: }
354: convertedValue = StringUtils
355: .arrayToCommaDelimitedString((String[]) convertedValue);
356: }
357:
358: if (editor != null && convertedValue instanceof String) {
359: // Use PropertyEditor's setAsText in case of a String value.
360: if (logger.isTraceEnabled()) {
361: logger.trace("Converting String to [" + requiredType
362: + "] using property editor [" + editor + "]");
363: }
364: String newTextValue = (String) convertedValue;
365: if (sharedEditor) {
366: // Synchronized access to shared editor instance.
367: synchronized (editor) {
368: return doConvertTextValue(oldValue, newTextValue,
369: editor);
370: }
371: } else {
372: // Unsynchronized access to non-shared editor instance.
373: return doConvertTextValue(oldValue, newTextValue,
374: editor);
375: }
376: }
377:
378: return convertedValue;
379: }
380:
381: /**
382: * Convert the given text value using the given property editor.
383: * @param oldValue the previous value, if available (may be <code>null</code>)
384: * @param newTextValue the proposed text value
385: * @param editor the PropertyEditor to use
386: * @return the converted value
387: */
388: protected Object doConvertTextValue(Object oldValue,
389: String newTextValue, PropertyEditor editor) {
390: editor.setValue(oldValue);
391: editor.setAsText(newTextValue);
392: return editor.getValue();
393: }
394:
395: protected Object convertToTypedArray(Object input,
396: String propertyName, Class componentType) {
397: if (input instanceof Collection) {
398: // Convert Collection elements to array elements.
399: Collection coll = (Collection) input;
400: Object result = Array.newInstance(componentType, coll
401: .size());
402: int i = 0;
403: for (Iterator it = coll.iterator(); it.hasNext(); i++) {
404: Object value = convertIfNecessary(
405: buildIndexedPropertyName(propertyName, i),
406: null, it.next(), componentType);
407: Array.set(result, i, value);
408: }
409: return result;
410: } else if (input.getClass().isArray()) {
411: // Convert array elements, if necessary.
412: if (componentType.equals(input.getClass()
413: .getComponentType())
414: && !this .propertyEditorRegistry
415: .hasCustomEditorForElement(componentType,
416: propertyName)) {
417: return input;
418: }
419: int arrayLength = Array.getLength(input);
420: Object result = Array.newInstance(componentType,
421: arrayLength);
422: for (int i = 0; i < arrayLength; i++) {
423: Object value = convertIfNecessary(
424: buildIndexedPropertyName(propertyName, i),
425: null, Array.get(input, i), componentType);
426: Array.set(result, i, value);
427: }
428: return result;
429: } else {
430: // A plain value: convert it to an array with a single component.
431: Object result = Array.newInstance(componentType, 1);
432: Object value = convertIfNecessary(buildIndexedPropertyName(
433: propertyName, 0), null, input, componentType);
434: Array.set(result, 0, value);
435: return result;
436: }
437: }
438:
439: protected Collection convertToTypedCollection(Collection original,
440: String propertyName, MethodParameter methodParam) {
441:
442: Class elementType = null;
443: if (methodParam != null && JdkVersion.isAtLeastJava15()) {
444: elementType = GenericCollectionTypeResolver
445: .getCollectionParameterType(methodParam);
446: }
447: if (elementType == null
448: && !this .propertyEditorRegistry
449: .hasCustomEditorForElement(null, propertyName)) {
450: return original;
451: }
452:
453: Collection convertedCopy = null;
454: Iterator it = null;
455: try {
456: it = original.iterator();
457: if (it == null) {
458: if (logger.isDebugEnabled()) {
459: logger
460: .debug("Collection of type ["
461: + original.getClass().getName()
462: + "] returned null Iterator - injecting original Collection as-is");
463: }
464: return original;
465: }
466: convertedCopy = CollectionFactory
467: .createApproximateCollection(original, original
468: .size());
469: } catch (Throwable ex) {
470: if (logger.isDebugEnabled()) {
471: logger
472: .debug(
473: "Cannot access Collection of type ["
474: + original.getClass().getName()
475: + "] - injecting original Collection as-is",
476: ex);
477: }
478: return original;
479: }
480: boolean actuallyConverted = false;
481: int i = 0;
482: for (; it.hasNext(); i++) {
483: Object element = it.next();
484: String indexedPropertyName = buildIndexedPropertyName(
485: propertyName, i);
486: if (methodParam != null) {
487: methodParam.increaseNestingLevel();
488: }
489: Object convertedElement = convertIfNecessary(
490: indexedPropertyName, null, element, elementType,
491: null, methodParam);
492: if (methodParam != null) {
493: methodParam.decreaseNestingLevel();
494: }
495: convertedCopy.add(convertedElement);
496: actuallyConverted = actuallyConverted
497: || (element != convertedElement);
498: }
499: return (actuallyConverted ? convertedCopy : original);
500: }
501:
502: protected Map convertToTypedMap(Map original, String propertyName,
503: MethodParameter methodParam) {
504: Class keyType = null;
505: Class valueType = null;
506: if (methodParam != null && JdkVersion.isAtLeastJava15()) {
507: keyType = GenericCollectionTypeResolver
508: .getMapKeyParameterType(methodParam);
509: valueType = GenericCollectionTypeResolver
510: .getMapValueParameterType(methodParam);
511: }
512: if (keyType == null
513: && valueType == null
514: && !this .propertyEditorRegistry
515: .hasCustomEditorForElement(null, propertyName)) {
516: return original;
517: }
518:
519: Map convertedCopy = null;
520: Iterator it = null;
521: try {
522: it = original.entrySet().iterator();
523: if (it == null) {
524: if (logger.isDebugEnabled()) {
525: logger
526: .debug("Map of type ["
527: + original.getClass().getName()
528: + "] returned null Iterator - injecting original Map as-is");
529: }
530: }
531: convertedCopy = CollectionFactory.createApproximateMap(
532: original, original.size());
533: } catch (Throwable ex) {
534: if (logger.isDebugEnabled()) {
535: logger.debug("Cannot access Map of type ["
536: + original.getClass().getName()
537: + "] - injecting original Map as-is", ex);
538: }
539: return original;
540: }
541: boolean actuallyConverted = false;
542: while (it.hasNext()) {
543: Map.Entry entry = (Map.Entry) it.next();
544: Object key = entry.getKey();
545: Object value = entry.getValue();
546: String keyedPropertyName = buildKeyedPropertyName(
547: propertyName, key);
548: if (methodParam != null) {
549: methodParam.increaseNestingLevel();
550: methodParam.setTypeIndexForCurrentLevel(0);
551: }
552: Object convertedKey = convertIfNecessary(keyedPropertyName,
553: null, key, keyType, null, methodParam);
554: if (methodParam != null) {
555: methodParam.setTypeIndexForCurrentLevel(1);
556: }
557: Object convertedValue = convertIfNecessary(
558: keyedPropertyName, null, value, valueType, null,
559: methodParam);
560: if (methodParam != null) {
561: methodParam.decreaseNestingLevel();
562: }
563: convertedCopy.put(convertedKey, convertedValue);
564: actuallyConverted = actuallyConverted
565: || (key != convertedKey)
566: || (value != convertedValue);
567: }
568: return (actuallyConverted ? convertedCopy : original);
569: }
570:
571: private String buildIndexedPropertyName(String propertyName,
572: int index) {
573: return (propertyName != null ? propertyName
574: + PropertyAccessor.PROPERTY_KEY_PREFIX + index
575: + PropertyAccessor.PROPERTY_KEY_SUFFIX : null);
576: }
577:
578: private String buildKeyedPropertyName(String propertyName,
579: Object key) {
580: return (propertyName != null ? propertyName
581: + PropertyAccessor.PROPERTY_KEY_PREFIX + key
582: + PropertyAccessor.PROPERTY_KEY_SUFFIX : null);
583: }
584:
585: }
|