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;
class TypeConverterDelegate {
private static final Log logger = LogFactory.getLog(TypeConverterDelegate.class);
private final PropertyEditorRegistrySupport propertyEditorRegistry;
@Nullable
private final Object targetObject;
public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry) {
this(propertyEditorRegistry, null);
}
public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry, @Nullable Object targetObject) {
this.propertyEditorRegistry = propertyEditorRegistry;
this.targetObject = targetObject;
}
@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));
}
@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 {
PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);
ConversionFailedException conversionAttemptEx = null;
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) {
conversionAttemptEx = ex;
}
}
}
Object convertedValue = newValue;
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) {
if (convertedValue != null) {
if (Object.class == requiredType) {
return (T) convertedValue;
}
else if (requiredType.isArray()) {
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) {
convertedValue = convertToTypedCollection(
(Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor);
standardConversion = true;
}
else if (convertedValue instanceof Map) {
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())) {
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) {
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()) {
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 {
if (requiredType == Optional.class) {
convertedValue = Optional.empty();
}
}
if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) {
if (conversionAttemptEx != null) {
throw conversionAttemptEx;
}
else if (conversionService != null && typeDescriptor != null) {
TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
}
}
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) {
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 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;
}
@Nullable
private PropertyEditor findDefaultEditor(@Nullable Class<?> requiredType) {
PropertyEditor editor = null;
if (requiredType != null) {
editor = this.propertyEditorRegistry.getDefaultEditor(requiredType);
if (editor == null && String.class != requiredType) {
editor = BeanUtils.findEditorByConvention(requiredType);
}
}
return editor;
}
@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)) {
try {
editor.setValue(convertedValue);
Object newConvertedValue = editor.getValue();
if (newConvertedValue != convertedValue) {
convertedValue = newConvertedValue;
editor = null;
}
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("PropertyEditor [" + editor.getClass().getName() + "] does not support setValue call", ex);
}
}
}
Object returnValue = convertedValue;
if (requiredType != null && !requiredType.isArray() && convertedValue instanceof String[]) {
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) {
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;
}
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);
}
}
editor.setAsText(newTextValue);
return editor.getValue();
}
private Object convertToTypedArray(Object input, @Nullable String propertyName, Class<?> componentType) {
if (input instanceof Collection) {
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()) {
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 {
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));
}
}