/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.beans;

import java.beans.PropertyEditor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

Internal helper class for converting property values to target types.

Works on a given PropertyEditorRegistrySupport instance. Used as a delegate by BeanWrapperImpl and SimpleTypeConverter.

Author:Juergen Hoeller, Rob Harrop, Dave Syer
See Also:
Since:2.0
/** * Internal helper class for converting property values to target types. * * <p>Works on a given {@link PropertyEditorRegistrySupport} instance. * Used as a delegate by {@link BeanWrapperImpl} and {@link SimpleTypeConverter}. * * @author Juergen Hoeller * @author Rob Harrop * @author Dave Syer * @since 2.0 * @see BeanWrapperImpl * @see SimpleTypeConverter */
class TypeConverterDelegate { private static final Log logger = LogFactory.getLog(TypeConverterDelegate.class); private final PropertyEditorRegistrySupport propertyEditorRegistry; @Nullable private final Object targetObject;
Create a new TypeConverterDelegate for the given editor registry.
Params:
  • propertyEditorRegistry – the editor registry to use
/** * Create a new TypeConverterDelegate for the given editor registry. * @param propertyEditorRegistry the editor registry to use */
public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry) { this(propertyEditorRegistry, null); }
Create a new TypeConverterDelegate for the given editor registry and bean instance.
Params:
  • propertyEditorRegistry – the editor registry to use
  • targetObject – the target object to work on (as context that can be passed to editors)
/** * Create a new TypeConverterDelegate for the given editor registry and bean instance. * @param propertyEditorRegistry the editor registry to use * @param targetObject the target object to work on (as context that can be passed to editors) */
public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry, @Nullable Object targetObject) { this.propertyEditorRegistry = propertyEditorRegistry; this.targetObject = targetObject; }
Convert the value to the required type for the specified property.
Params:
  • propertyName – name of the property
  • oldValue – the previous value, if available (may be null)
  • newValue – the proposed new value
  • requiredType – the type we must convert to (or null if not known, for example in case of a collection element)
Throws:
Returns:the new value, possibly the result of type conversion
/** * Convert the value to the required type for the specified property. * @param propertyName name of the property * @param oldValue the previous value, if available (may be {@code null}) * @param newValue the proposed new value * @param requiredType the type we must convert to * (or {@code null} if not known, for example in case of a collection element) * @return the new value, possibly the result of type conversion * @throws IllegalArgumentException if type conversion failed */
@Nullable public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, Object newValue, @Nullable Class<T> requiredType) throws IllegalArgumentException { return convertIfNecessary(propertyName, oldValue, newValue, requiredType, TypeDescriptor.valueOf(requiredType)); }
Convert the value to the required type (if necessary from a String), for the specified property.
Params:
  • propertyName – name of the property
  • oldValue – the previous value, if available (may be null)
  • newValue – the proposed new value
  • requiredType – the type we must convert to (or null if not known, for example in case of a collection element)
  • typeDescriptor – the descriptor for the target property or field
Throws:
Returns:the new value, possibly the result of type conversion
/** * Convert the value to the required type (if necessary from a String), * for the specified property. * @param propertyName name of the property * @param oldValue the previous value, if available (may be {@code null}) * @param newValue the proposed new value * @param requiredType the type we must convert to * (or {@code null} if not known, for example in case of a collection element) * @param typeDescriptor the descriptor for the target property or field * @return the new value, possibly the result of type conversion * @throws IllegalArgumentException if type conversion failed */
@SuppressWarnings("unchecked") @Nullable public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException { // Custom editor for this type? PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName); ConversionFailedException conversionAttemptEx = null; // No custom editor but custom ConversionService specified? ConversionService conversionService = this.propertyEditorRegistry.getConversionService(); if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) { TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { try { return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } catch (ConversionFailedException ex) { // fallback to default conversion logic below conversionAttemptEx = ex; } } } Object convertedValue = newValue; // Value not of required type? if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) { if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) && convertedValue instanceof String) { TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor(); if (elementTypeDesc != null) { Class<?> elementType = elementTypeDesc.getType(); if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) { convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue); } } } if (editor == null) { editor = findDefaultEditor(requiredType); } convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor); } boolean standardConversion = false; if (requiredType != null) { // Try to apply some standard type conversion rules if appropriate. if (convertedValue != null) { if (Object.class == requiredType) { return (T) convertedValue; } else if (requiredType.isArray()) { // Array required -> apply appropriate conversion of elements. if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) { convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue); } return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType()); } else if (convertedValue instanceof Collection) { // Convert elements to target type, if determined. convertedValue = convertToTypedCollection( (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor); standardConversion = true; } else if (convertedValue instanceof Map) { // Convert keys and values to respective target type, if determined. convertedValue = convertToTypedMap( (Map<?, ?>) convertedValue, propertyName, requiredType, typeDescriptor); standardConversion = true; } if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) { convertedValue = Array.get(convertedValue, 0); standardConversion = true; } if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) { // We can stringify any primitive value... return (T) convertedValue.toString(); } else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) { if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) { try { Constructor<T> strCtor = requiredType.getConstructor(String.class); return BeanUtils.instantiateClass(strCtor, convertedValue); } catch (NoSuchMethodException ex) { // proceed with field lookup if (logger.isTraceEnabled()) { logger.trace("No String constructor found on type [" + requiredType.getName() + "]", ex); } } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Construction via String failed for type [" + requiredType.getName() + "]", ex); } } } String trimmedValue = ((String) convertedValue).trim(); if (requiredType.isEnum() && trimmedValue.isEmpty()) { // It's an empty enum identifier: reset the enum value to null. return null; } convertedValue = attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue); standardConversion = true; } else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) { convertedValue = NumberUtils.convertNumberToTargetClass( (Number) convertedValue, (Class<Number>) requiredType); standardConversion = true; } } else { // convertedValue == null if (requiredType == Optional.class) { convertedValue = Optional.empty(); } } if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) { if (conversionAttemptEx != null) { // Original exception from former ConversionService call above... throw conversionAttemptEx; } else if (conversionService != null && typeDescriptor != null) { // ConversionService not tried before, probably custom editor found // but editor couldn't produce the required type... TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } } // Definitely doesn't match: throw IllegalArgumentException/IllegalStateException StringBuilder msg = new StringBuilder(); msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue)); msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append("'"); if (propertyName != null) { msg.append(" for property '").append(propertyName).append("'"); } if (editor != null) { msg.append(": PropertyEditor [").append(editor.getClass().getName()).append( "] returned inappropriate value of type '").append( ClassUtils.getDescriptiveType(convertedValue)).append("'"); throw new IllegalArgumentException(msg.toString()); } else { msg.append(": no matching editors or conversion strategy found"); throw new IllegalStateException(msg.toString()); } } } if (conversionAttemptEx != null) { if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) { throw conversionAttemptEx; } logger.debug("Original ConversionService attempt failed - ignored since " + "PropertyEditor based conversion eventually succeeded", conversionAttemptEx); } return (T) convertedValue; } private Object attemptToConvertStringToEnum(Class<?> requiredType, String trimmedValue, Object currentConvertedValue) { Object convertedValue = currentConvertedValue; if (Enum.class == requiredType && this.targetObject != null) { // target type is declared as raw enum, treat the trimmed value as <enum.fqn>.FIELD_NAME int index = trimmedValue.lastIndexOf('.'); if (index > - 1) { String enumType = trimmedValue.substring(0, index); String fieldName = trimmedValue.substring(index + 1); ClassLoader cl = this.targetObject.getClass().getClassLoader(); try { Class<?> enumValueType = ClassUtils.forName(enumType, cl); Field enumField = enumValueType.getField(fieldName); convertedValue = enumField.get(null); } catch (ClassNotFoundException ex) { if (logger.isTraceEnabled()) { logger.trace("Enum class [" + enumType + "] cannot be loaded", ex); } } catch (Throwable ex) { if (logger.isTraceEnabled()) { logger.trace("Field [" + fieldName + "] isn't an enum value for type [" + enumType + "]", ex); } } } } if (convertedValue == currentConvertedValue) { // Try field lookup as fallback: for JDK 1.5 enum or custom enum // with values defined as static fields. Resulting value still needs // to be checked, hence we don't return it right away. try { Field enumField = requiredType.getField(trimmedValue); ReflectionUtils.makeAccessible(enumField); convertedValue = enumField.get(null); } catch (Throwable ex) { if (logger.isTraceEnabled()) { logger.trace("Field [" + convertedValue + "] isn't an enum value", ex); } } } return convertedValue; }
Find a default editor for the given type.
Params:
  • requiredType – the type to find an editor for
Returns:the corresponding editor, or null if none
/** * Find a default editor for the given type. * @param requiredType the type to find an editor for * @return the corresponding editor, or {@code null} if none */
@Nullable private PropertyEditor findDefaultEditor(@Nullable Class<?> requiredType) { PropertyEditor editor = null; if (requiredType != null) { // No custom editor -> check BeanWrapperImpl's default editors. editor = this.propertyEditorRegistry.getDefaultEditor(requiredType); if (editor == null && String.class != requiredType) { // No BeanWrapper default editor -> check standard JavaBean editor. editor = BeanUtils.findEditorByConvention(requiredType); } } return editor; }
Convert the value to the required type (if necessary from a String), using the given property editor.
Params:
  • oldValue – the previous value, if available (may be null)
  • newValue – the proposed new value
  • requiredType – the type we must convert to (or null if not known, for example in case of a collection element)
  • editor – the PropertyEditor to use
Throws:
Returns:the new value, possibly the result of type conversion
/** * Convert the value to the required type (if necessary from a String), * using the given property editor. * @param oldValue the previous value, if available (may be {@code null}) * @param newValue the proposed new value * @param requiredType the type we must convert to * (or {@code null} if not known, for example in case of a collection element) * @param editor the PropertyEditor to use * @return the new value, possibly the result of type conversion * @throws IllegalArgumentException if type conversion failed */
@Nullable private Object doConvertValue(@Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<?> requiredType, @Nullable PropertyEditor editor) { Object convertedValue = newValue; if (editor != null && !(convertedValue instanceof String)) { // Not a String -> use PropertyEditor's setValue. // With standard PropertyEditors, this will return the very same object; // we just want to allow special PropertyEditors to override setValue // for type conversion from non-String values to the required type. try { editor.setValue(convertedValue); Object newConvertedValue = editor.getValue(); if (newConvertedValue != convertedValue) { convertedValue = newConvertedValue; // Reset PropertyEditor: It already did a proper conversion. // Don't use it again for a setAsText call. editor = null; } } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("PropertyEditor [" + editor.getClass().getName() + "] does not support setValue call", ex); } // Swallow and proceed. } } Object returnValue = convertedValue; if (requiredType != null && !requiredType.isArray() && convertedValue instanceof String[]) { // Convert String array to a comma-separated String. // Only applies if no PropertyEditor converted the String array before. // The CSV String will be passed into a PropertyEditor's setAsText method, if any. if (logger.isTraceEnabled()) { logger.trace("Converting String array to comma-delimited String [" + convertedValue + "]"); } convertedValue = StringUtils.arrayToCommaDelimitedString((String[]) convertedValue); } if (convertedValue instanceof String) { if (editor != null) { // Use PropertyEditor's setAsText in case of a String value. if (logger.isTraceEnabled()) { logger.trace("Converting String to [" + requiredType + "] using property editor [" + editor + "]"); } String newTextValue = (String) convertedValue; return doConvertTextValue(oldValue, newTextValue, editor); } else if (String.class == requiredType) { returnValue = convertedValue; } } return returnValue; }
Convert the given text value using the given property editor.
Params:
  • oldValue – the previous value, if available (may be null)
  • newTextValue – the proposed text value
  • editor – the PropertyEditor to use
Returns:the converted value
/** * Convert the given text value using the given property editor. * @param oldValue the previous value, if available (may be {@code null}) * @param newTextValue the proposed text value * @param editor the PropertyEditor to use * @return the converted value */
private Object doConvertTextValue(@Nullable Object oldValue, String newTextValue, PropertyEditor editor) { try { editor.setValue(oldValue); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("PropertyEditor [" + editor.getClass().getName() + "] does not support setValue call", ex); } // Swallow and proceed. } editor.setAsText(newTextValue); return editor.getValue(); } private Object convertToTypedArray(Object input, @Nullable String propertyName, Class<?> componentType) { if (input instanceof Collection) { // Convert Collection elements to array elements. Collection<?> coll = (Collection<?>) input; Object result = Array.newInstance(componentType, coll.size()); int i = 0; for (Iterator<?> it = coll.iterator(); it.hasNext(); i++) { Object value = convertIfNecessary( buildIndexedPropertyName(propertyName, i), null, it.next(), componentType); Array.set(result, i, value); } return result; } else if (input.getClass().isArray()) { // Convert array elements, if necessary. if (componentType.equals(input.getClass().getComponentType()) && !this.propertyEditorRegistry.hasCustomEditorForElement(componentType, propertyName)) { return input; } int arrayLength = Array.getLength(input); Object result = Array.newInstance(componentType, arrayLength); for (int i = 0; i < arrayLength; i++) { Object value = convertIfNecessary( buildIndexedPropertyName(propertyName, i), null, Array.get(input, i), componentType); Array.set(result, i, value); } return result; } else { // A plain value: convert it to an array with a single component. Object result = Array.newInstance(componentType, 1); Object value = convertIfNecessary( buildIndexedPropertyName(propertyName, 0), null, input, componentType); Array.set(result, 0, value); return result; } } @SuppressWarnings("unchecked") private Collection<?> convertToTypedCollection(Collection<?> original, @Nullable String propertyName, Class<?> requiredType, @Nullable TypeDescriptor typeDescriptor) { if (!Collection.class.isAssignableFrom(requiredType)) { return original; } boolean approximable = CollectionFactory.isApproximableCollectionType(requiredType); if (!approximable && !canCreateCopy(requiredType)) { if (logger.isDebugEnabled()) { logger.debug("Custom Collection type [" + original.getClass().getName() + "] does not allow for creating a copy - injecting original Collection as-is"); } return original; } boolean originalAllowed = requiredType.isInstance(original); TypeDescriptor elementType = (typeDescriptor != null ? typeDescriptor.getElementTypeDescriptor() : null); if (elementType == null && originalAllowed && !this.propertyEditorRegistry.hasCustomEditorForElement(null, propertyName)) { return original; } Iterator<?> it; try { it = original.iterator(); } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Cannot access Collection of type [" + original.getClass().getName() + "] - injecting original Collection as-is: " + ex); } return original; } Collection<Object> convertedCopy; try { if (approximable) { convertedCopy = CollectionFactory.createApproximateCollection(original, original.size()); } else { convertedCopy = (Collection<Object>) ReflectionUtils.accessibleConstructor(requiredType).newInstance(); } } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Cannot create copy of Collection type [" + original.getClass().getName() + "] - injecting original Collection as-is: " + ex); } return original; } for (int i = 0; it.hasNext(); i++) { Object element = it.next(); String indexedPropertyName = buildIndexedPropertyName(propertyName, i); Object convertedElement = convertIfNecessary(indexedPropertyName, null, element, (elementType != null ? elementType.getType() : null) , elementType); try { convertedCopy.add(convertedElement); } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Collection type [" + original.getClass().getName() + "] seems to be read-only - injecting original Collection as-is: " + ex); } return original; } originalAllowed = originalAllowed && (element == convertedElement); } return (originalAllowed ? original : convertedCopy); } @SuppressWarnings("unchecked") private Map<?, ?> convertToTypedMap(Map<?, ?> original, @Nullable String propertyName, Class<?> requiredType, @Nullable TypeDescriptor typeDescriptor) { if (!Map.class.isAssignableFrom(requiredType)) { return original; } boolean approximable = CollectionFactory.isApproximableMapType(requiredType); if (!approximable && !canCreateCopy(requiredType)) { if (logger.isDebugEnabled()) { logger.debug("Custom Map type [" + original.getClass().getName() + "] does not allow for creating a copy - injecting original Map as-is"); } return original; } boolean originalAllowed = requiredType.isInstance(original); TypeDescriptor keyType = (typeDescriptor != null ? typeDescriptor.getMapKeyTypeDescriptor() : null); TypeDescriptor valueType = (typeDescriptor != null ? typeDescriptor.getMapValueTypeDescriptor() : null); if (keyType == null && valueType == null && originalAllowed && !this.propertyEditorRegistry.hasCustomEditorForElement(null, propertyName)) { return original; } Iterator<?> it; try { it = original.entrySet().iterator(); } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Cannot access Map of type [" + original.getClass().getName() + "] - injecting original Map as-is: " + ex); } return original; } Map<Object, Object> convertedCopy; try { if (approximable) { convertedCopy = CollectionFactory.createApproximateMap(original, original.size()); } else { convertedCopy = (Map<Object, Object>) ReflectionUtils.accessibleConstructor(requiredType).newInstance(); } } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Cannot create copy of Map type [" + original.getClass().getName() + "] - injecting original Map as-is: " + ex); } return original; } while (it.hasNext()) { Map.Entry<?, ?> entry = (Map.Entry<?, ?>) it.next(); Object key = entry.getKey(); Object value = entry.getValue(); String keyedPropertyName = buildKeyedPropertyName(propertyName, key); Object convertedKey = convertIfNecessary(keyedPropertyName, null, key, (keyType != null ? keyType.getType() : null), keyType); Object convertedValue = convertIfNecessary(keyedPropertyName, null, value, (valueType!= null ? valueType.getType() : null), valueType); try { convertedCopy.put(convertedKey, convertedValue); } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Map type [" + original.getClass().getName() + "] seems to be read-only - injecting original Map as-is: " + ex); } return original; } originalAllowed = originalAllowed && (key == convertedKey) && (value == convertedValue); } return (originalAllowed ? original : convertedCopy); } @Nullable private String buildIndexedPropertyName(@Nullable String propertyName, int index) { return (propertyName != null ? propertyName + PropertyAccessor.PROPERTY_KEY_PREFIX + index + PropertyAccessor.PROPERTY_KEY_SUFFIX : null); } @Nullable private String buildKeyedPropertyName(@Nullable String propertyName, Object key) { return (propertyName != null ? propertyName + PropertyAccessor.PROPERTY_KEY_PREFIX + key + PropertyAccessor.PROPERTY_KEY_SUFFIX : null); } private boolean canCreateCopy(Class<?> requiredType) { return (!requiredType.isInterface() && !Modifier.isAbstract(requiredType.getModifiers()) && Modifier.isPublic(requiredType.getModifiers()) && ClassUtils.hasConstructor(requiredType)); } }