Merge pull request #132 from philwebb/SPR-9692

# By Phillip Webb (6) and Chris Beams (1)
* SPR-9692:
  Review and polish pull request #132
  Support conversion from Enum Interface
  Test SpEL unconditional argument conversion
  Bypass conversion when possible
  Extend conditional conversion support
  Refactor GenericConversionService
  Additional GenericConversionService Tests
This commit is contained in:
Chris Beams 2012-10-30 09:15:57 +01:00
commit a57ff506f2
16 changed files with 817 additions and 400 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@ -21,6 +21,7 @@ package org.springframework.core.convert;
* Call {@link #convert(Object, Class)} to perform a thread-safe type conversion using this system.
*
* @author Keith Donald
* @author Phillip Webb
* @since 3.0
*/
public interface ConversionService {
@ -54,6 +55,30 @@ public interface ConversionService {
*/
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
/**
* Returns true if conversion between the sourceType and targetType can be bypassed.
* More precisely this method will return true if objects of sourceType can be
* converted to the targetType by returning the source object unchanged.
* @param sourceType context about the source type to convert from (may be null if source is null)
* @param targetType context about the target type to convert to (required)
* @return true if conversion can be bypassed
* @throws IllegalArgumentException if targetType is null
* @since 3.2
*/
boolean canBypassConvert(Class<?> sourceType, Class<?> targetType);
/**
* Returns true if conversion between the sourceType and targetType can be bypassed.
* More precisely this method will return true if objects of sourceType can be
* converted to the targetType by returning the source object unchanged.
* @param sourceType context about the source type to convert from (may be null if source is null)
* @param targetType context about the target type to convert to (required)
* @return true if conversion can be bypassed
* @throws IllegalArgumentException if targetType is null
* @since 3.2
*/
boolean canBypassConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
/**
* Convert the source to targetType.
* @param source the source object to convert (may be null)

View File

@ -23,6 +23,7 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@ -249,6 +250,24 @@ public class TypeDescriptor {
this.mapKeyTypeDescriptor, this.mapValueTypeDescriptor, this.annotations);
}
/**
* Cast this {@link TypeDescriptor} to a superclass or implemented interface
* preserving annotations and nested type context.
*
* @param superType the super type to cast to (can be {@code null}
* @return a new TypeDescriptor for the up-cast type
* @throws IllegalArgumentException if this type is not assignable to the super-type
* @since 3.2
*/
public TypeDescriptor upcast(Class<?> superType) {
if (superType == null) {
return null;
}
Assert.isAssignable(superType, getType());
return new TypeDescriptor(superType, this.elementTypeDescriptor,
this.mapKeyTypeDescriptor, this.mapValueTypeDescriptor, this.annotations);
}
/**
* Returns the name of this type: the fully qualified class name.
*/

View File

@ -0,0 +1,55 @@
/*
* Copyright 2002-2012 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
*
* http://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.core.convert.converter;
import org.springframework.core.convert.TypeDescriptor;
/**
* Allows a {@link Converter}, {@link GenericConverter} or {@link ConverterFactory} to
* conditionally execute based on attributes of the {@code source} and {@code target}
* {@link TypeDescriptor}.
*
* <p>Often used to selectively match custom conversion logic based on the presence of a
* field or class-level characteristic, such as an annotation or method. For example, when
* converting from a String field to a Date field, an implementation might return
*
* {@code true} if the target field has also been annotated with {@code @DateTimeFormat}.
*
* <p>As another example, when converting from a String field to an {@code Account} field, an
* implementation might return {@code true} if the target Account class defines a
* {@code public static findAccount(String)} method.
*
* @author Phillip Webb
* @author Keith Donald
* @since 3.2
* @see Converter
* @see GenericConverter
* @see ConverterFactory
* @see ConditionalGenericConverter
*/
public interface ConditionalConverter {
/**
* Should the conversion from {@code sourceType} to {@code targetType} currently under
* consideration be selected?
*
* @param sourceType the type descriptor of the field we are converting from
* @param targetType the type descriptor of the field we are converting to
* @return true if conversion should be performed, false otherwise
*/
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2012 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.
@ -19,33 +19,17 @@ package org.springframework.core.convert.converter;
import org.springframework.core.convert.TypeDescriptor;
/**
* A generic converter that conditionally executes.
*
* <p>Applies a rule that determines if a converter between a set of
* {@link #getConvertibleTypes() convertible types} matches given a client request to
* convert between a source field of convertible type S and a target field of convertible type T.
*
* <p>Often used to selectively match custom conversion logic based on the presence of
* a field or class-level characteristic, such as an annotation or method. For example,
* when converting from a String field to a Date field, an implementation might return
* <code>true</code> if the target field has also been annotated with <code>@DateTimeFormat</code>.
*
* <p>As another example, when converting from a String field to an Account field,
* an implementation might return true if the target Account class defines a
* <code>public static findAccount(String)</code> method.
* A {@link GenericConverter} that may conditionally execute based on attributes of the
* {@code source} and {@code target} {@link TypeDescriptor}. See
* {@link ConditionalConverter} for details.
*
* @author Keith Donald
* @author Phillip Webb
* @since 3.0
* @see GenericConverter
* @see ConditionalConverter
*/
public interface ConditionalGenericConverter extends GenericConverter {
/**
* Should the converter from <code>sourceType</code> to <code>targetType</code>
* currently under consideration be selected?
* @param sourceType the type descriptor of the field we are converting from
* @param targetType the type descriptor of the field we are converting to
* @return true if conversion should be performed, false otherwise
*/
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
public interface ConditionalGenericConverter extends GenericConverter,
ConditionalConverter {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2012 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.
@ -20,10 +20,13 @@ package org.springframework.core.convert.converter;
* A converter converts a source object of type S to a target of type T.
* Implementations of this interface are thread-safe and can be shared.
*
* <p>Implementations may additionally implement {@link ConditionalConverter}.
*
* @author Keith Donald
* @since 3.0
* @see ConditionalConverter
* @param <S> The source type
* @param <T> The target type
* @since 3.0
*/
public interface Converter<S, T> {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2012 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.
@ -19,8 +19,11 @@ package org.springframework.core.convert.converter;
/**
* A factory for "ranged" converters that can convert objects from S to subtypes of R.
*
* <p>Implementations may additionally implement {@link ConditionalConverter}.
*
* @author Keith Donald
* @since 3.0
* @see ConditionalConverter
* @param <S> The source type converters created by this factory can convert from
* @param <R> The target range (or base) type converters created by this factory can convert to;
* for example {@link Number} for a set of number subtypes.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@ -34,18 +34,24 @@ import java.util.Set;
* <p>This interface should generally not be used when the simpler {@link Converter} or
* {@link ConverterFactory} interfaces are sufficient.
*
* <p>Implementations may additionally implement {@link ConditionalConverter}.
*
* @author Keith Donald
* @author Juergen Hoeller
* @since 3.0
* @see TypeDescriptor
* @see Converter
* @see ConverterFactory
* @see ConditionalConverter
*/
public interface GenericConverter {
/**
* Return the source and target types which this converter can convert between.
* <p>Each entry is a convertible source-to-target type pair.
* Return the source and target types which this converter can convert between. Each
* entry is a convertible source-to-target type pair.
* <p>
* For {@link ConditionalConverter conditional} converters this method may return
* {@code null} to indicate all source-to-target pairs should be considered. *
*/
Set<ConvertiblePair> getConvertibleTypes();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@ -18,6 +18,7 @@ package org.springframework.core.convert.support;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.springframework.core.convert.ConversionService;
@ -26,18 +27,22 @@ import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.util.ObjectUtils;
/**
* Converts an Array to another Array.
* First adapts the source array to a List, then delegates to {@link CollectionToArrayConverter} to perform the target array conversion.
* Converts an Array to another Array. First adapts the source array to a List, then
* delegates to {@link CollectionToArrayConverter} to perform the target array conversion.
*
* @author Keith Donald
* @author Phillip Webb
* @since 3.0
*/
final class ArrayToArrayConverter implements ConditionalGenericConverter {
private final CollectionToArrayConverter helperConverter;
private final ConversionService conversionService;
public ArrayToArrayConverter(ConversionService conversionService) {
this.helperConverter = new CollectionToArrayConverter(conversionService);
this.conversionService = conversionService;
}
public Set<ConvertiblePair> getConvertibleTypes() {
@ -48,8 +53,14 @@ final class ArrayToArrayConverter implements ConditionalGenericConverter {
return this.helperConverter.matches(sourceType, targetType);
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return this.helperConverter.convert(Arrays.asList(ObjectUtils.toObjectArray(source)), sourceType, targetType);
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
if (conversionService.canBypassConvert(sourceType.getElementTypeDescriptor(),
targetType.getElementTypeDescriptor())) {
return source;
}
List<Object> sourceList = Arrays.asList(ObjectUtils.toObjectArray(source));
return this.helperConverter.convert(sourceList, sourceType, targetType);
}
}

View File

@ -59,6 +59,7 @@ public class DefaultConversionService extends GenericConversionService {
// internal helpers
private static void addScalarConverters(ConverterRegistry converterRegistry) {
ConversionService conversionService = (ConversionService) converterRegistry;
converterRegistry.addConverter(new StringToBooleanConverter());
converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());
@ -74,7 +75,7 @@ public class DefaultConversionService extends GenericConversionService {
converterRegistry.addConverterFactory(new CharacterToNumberFactory());
converterRegistry.addConverterFactory(new StringToEnumConverterFactory());
converterRegistry.addConverter(Enum.class, String.class, new EnumToStringConverter());
converterRegistry.addConverter(Enum.class, String.class, new EnumToStringConverter(conversionService));
converterRegistry.addConverter(new StringToLocaleConverter());
converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2012 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.
@ -16,14 +16,35 @@
package org.springframework.core.convert.support;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.ClassUtils;
/**
* Simply calls {@link Enum#name()} to convert a source Enum to a String.
* Calls {@link Enum#name()} to convert a source Enum to a String. This converter will
* not match enums with interfaces that can be converterd.
* @author Keith Donald
* @author Phillip Webb
* @since 3.0
*/
final class EnumToStringConverter implements Converter<Enum<?>, String> {
final class EnumToStringConverter implements Converter<Enum<?>, String>, ConditionalConverter {
private final ConversionService conversionService;
public EnumToStringConverter(ConversionService conversionService) {
this.conversionService = conversionService;
}
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
for (Class<?> interfaceType : ClassUtils.getAllInterfacesForClass(sourceType.getType())) {
if (conversionService.canConvert(TypeDescriptor.valueOf(interfaceType), targetType)) {
return false;
}
}
return true;
}
public String convert(Enum<?> source) {
return source.name();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2012 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.
@ -41,6 +41,9 @@ final class FallbackObjectToStringConverter implements ConditionalGenericConvert
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
Class<?> sourceClass = sourceType.getObjectType();
if (String.class.equals(sourceClass)) {
return false;
}
return CharSequence.class.isAssignableFrom(sourceClass) || StringWriter.class.isAssignableFrom(sourceClass) ||
ObjectToObjectConverter.hasValueOfMethodOrConstructor(sourceClass, String.class);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@ -19,9 +19,8 @@ package org.springframework.core.convert.support;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
@ -35,13 +34,17 @@ import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base {@link ConversionService} implementation suitable for use in most environments.
@ -51,37 +54,24 @@ import org.springframework.util.ClassUtils;
* @author Keith Donald
* @author Juergen Hoeller
* @author Chris Beams
* @author Phillip Webb
* @since 3.0
*/
public class GenericConversionService implements ConfigurableConversionService {
private static final GenericConverter NO_OP_CONVERTER = new GenericConverter() {
public Set<ConvertiblePair> getConvertibleTypes() {
return null;
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return source;
}
public String toString() {
return "NO_OP";
}
};
/**
* General NO-OP converter used when conversion is not required.
*/
private static final GenericConverter NO_OP_CONVERTER = new NoOpConverter("NO_OP");
private static final GenericConverter NO_MATCH = new GenericConverter() {
public Set<ConvertiblePair> getConvertibleTypes() {
throw new UnsupportedOperationException();
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
throw new UnsupportedOperationException();
}
public String toString() {
return "NO_MATCH";
}
};
/**
* Used as a cache entry when no converter is available. This converter is never
* returned.
*/
private static final GenericConverter NO_MATCH = new NoOpConverter("NO_MATCH");
private final Map<Class<?>, Map<Class<?>, MatchableConverters>> converters =
new HashMap<Class<?>, Map<Class<?>, MatchableConverters>>(36);
private final Converters converters = new Converters();
private final Map<ConverterCacheKey, GenericConverter> converterCache =
new ConcurrentHashMap<ConverterCacheKey, GenericConverter>();
@ -91,10 +81,8 @@ public class GenericConversionService implements ConfigurableConversionService {
public void addConverter(Converter<?, ?> converter) {
GenericConverter.ConvertiblePair typeInfo = getRequiredTypeInfo(converter, Converter.class);
if (typeInfo == null) {
throw new IllegalArgumentException("Unable to the determine sourceType <S> and targetType <T> which " +
"your Converter<S, T> converts between; declare these generic types.");
}
Assert.notNull(typeInfo, "Unable to the determine sourceType <S> and targetType " +
"<T> which your Converter<S, T> converts between; declare these generic types.");
addConverter(new ConverterAdapter(typeInfo, converter));
}
@ -104,41 +92,36 @@ public class GenericConversionService implements ConfigurableConversionService {
}
public void addConverter(GenericConverter converter) {
Set<GenericConverter.ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
for (GenericConverter.ConvertiblePair convertibleType : convertibleTypes) {
getMatchableConverters(convertibleType.getSourceType(), convertibleType.getTargetType()).add(converter);
}
this.converters.add(converter);
invalidateCache();
}
public void addConverterFactory(ConverterFactory<?, ?> converterFactory) {
GenericConverter.ConvertiblePair typeInfo = getRequiredTypeInfo(converterFactory, ConverterFactory.class);
if (typeInfo == null) {
throw new IllegalArgumentException("Unable to the determine sourceType <S> and targetRangeType R which " +
"your ConverterFactory<S, R> converts between; declare these generic types.");
throw new IllegalArgumentException("Unable to the determine sourceType <S> and " +
"targetRangeType R which your ConverterFactory<S, R> converts between; " +
"declare these generic types.");
}
addConverter(new ConverterFactoryAdapter(typeInfo, converterFactory));
}
public void removeConvertible(Class<?> sourceType, Class<?> targetType) {
getSourceConverterMap(sourceType).remove(targetType);
this.converters.remove(sourceType, targetType);
invalidateCache();
}
// implementing ConversionService
public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
if (targetType == null) {
throw new IllegalArgumentException("The targetType to convert to cannot be null");
}
return canConvert(sourceType != null ? TypeDescriptor.valueOf(sourceType) : null, TypeDescriptor.valueOf(targetType));
Assert.notNull(targetType, "The targetType to convert to cannot be null");
return canConvert(sourceType != null ?
TypeDescriptor.valueOf(sourceType) : null,
TypeDescriptor.valueOf(targetType));
}
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (targetType == null) {
throw new IllegalArgumentException("The targetType to convert to cannot be null");
}
Assert.notNull(targetType,"The targetType to convert to cannot be null");
if (sourceType == null) {
return true;
}
@ -146,18 +129,30 @@ public class GenericConversionService implements ConfigurableConversionService {
return (converter != null);
}
public boolean canBypassConvert(Class<?> sourceType, Class<?> targetType) {
Assert.notNull(targetType, "The targetType to convert to cannot be null");
return canBypassConvert(sourceType != null ?
TypeDescriptor.valueOf(sourceType) : null,
TypeDescriptor.valueOf(targetType));
}
public boolean canBypassConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "The targetType to convert to cannot be null");
if (sourceType == null) {
return true;
}
GenericConverter converter = getConverter(sourceType, targetType);
return (converter == NO_OP_CONVERTER);
}
@SuppressWarnings("unchecked")
public <T> T convert(Object source, Class<T> targetType) {
if (targetType == null) {
throw new IllegalArgumentException("The targetType to convert to cannot be null");
}
Assert.notNull(targetType,"The targetType to convert to cannot be null");
return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (targetType == null) {
throw new IllegalArgumentException("The targetType to convert to cannot be null");
}
Assert.notNull(targetType,"The targetType to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "The source must be [null] if sourceType == [null]");
return handleResult(sourceType, targetType, convertNullSource(sourceType, targetType));
@ -171,14 +166,15 @@ public class GenericConversionService implements ConfigurableConversionService {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
else {
return handleConverterNotFound(source, sourceType, targetType);
}
return handleConverterNotFound(source, sourceType, targetType);
}
/**
* Convenience operation for converting a source object to the specified targetType, where the targetType is a descriptor that provides additional conversion context.
* Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and encapsulates the construction of the sourceType descriptor using {@link TypeDescriptor#forObject(Object)}.
* Convenience operation for converting a source object to the specified targetType,
* where the targetType is a descriptor that provides additional conversion context.
* Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and
* encapsulates the construction of the sourceType descriptor using
* {@link TypeDescriptor#forObject(Object)}.
* @param source the source object
* @param targetType the target type
* @return the converted value
@ -191,21 +187,7 @@ public class GenericConversionService implements ConfigurableConversionService {
}
public String toString() {
List<String> converterStrings = new ArrayList<String>();
for (Map<Class<?>, MatchableConverters> targetConverters : this.converters.values()) {
for (MatchableConverters matchable : targetConverters.values()) {
converterStrings.add(matchable.toString());
}
}
Collections.sort(converterStrings);
StringBuilder builder = new StringBuilder();
builder.append("ConversionService converters = ").append("\n");
for (String converterString : converterStrings) {
builder.append("\t");
builder.append(converterString);
builder.append("\n");
}
return builder.toString();
return this.converters.toString();
}
@ -231,7 +213,8 @@ public class GenericConversionService implements ConfigurableConversionService {
* Subclasses may override.
* @param sourceType the source type to convert from
* @param targetType the target type to convert to
* @return the generic converter that will perform the conversion, or <code>null</code> if no suitable converter was found
* @return the generic converter that will perform the conversion, or {@code null} if
* no suitable converter was found
* @see #getDefaultConverter(TypeDescriptor, TypeDescriptor)
*/
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
@ -240,20 +223,19 @@ public class GenericConversionService implements ConfigurableConversionService {
if (converter != null) {
return (converter != NO_MATCH ? converter : null);
}
else {
converter = findConverterForClassPair(sourceType, targetType);
if (converter == null) {
converter = getDefaultConverter(sourceType, targetType);
}
if (converter != null) {
this.converterCache.put(key, converter);
return converter;
}
else {
this.converterCache.put(key, NO_MATCH);
return null;
}
converter = this.converters.find(sourceType, targetType);
if (converter == null) {
converter = getDefaultConverter(sourceType, targetType);
}
if (converter != null) {
this.converterCache.put(key, converter);
return converter;
}
this.converterCache.put(key, NO_MATCH);
return null;
}
/**
@ -276,204 +258,19 @@ public class GenericConversionService implements ConfigurableConversionService {
return (args != null ? new GenericConverter.ConvertiblePair(args[0], args[1]) : null);
}
private MatchableConverters getMatchableConverters(Class<?> sourceType, Class<?> targetType) {
Map<Class<?>, MatchableConverters> sourceMap = getSourceConverterMap(sourceType);
MatchableConverters matchable = sourceMap.get(targetType);
if (matchable == null) {
matchable = new MatchableConverters();
sourceMap.put(targetType, matchable);
}
return matchable;
}
private void invalidateCache() {
this.converterCache.clear();
}
private Map<Class<?>, MatchableConverters> getSourceConverterMap(Class<?> sourceType) {
Map<Class<?>, MatchableConverters> sourceMap = converters.get(sourceType);
if (sourceMap == null) {
sourceMap = new HashMap<Class<?>, MatchableConverters>();
this.converters.put(sourceType, sourceMap);
}
return sourceMap;
}
private GenericConverter findConverterForClassPair(TypeDescriptor sourceType, TypeDescriptor targetType) {
Class<?> sourceObjectType = sourceType.getObjectType();
if (sourceObjectType.isInterface()) {
LinkedList<Class<?>> classQueue = new LinkedList<Class<?>>();
classQueue.addFirst(sourceObjectType);
while (!classQueue.isEmpty()) {
Class<?> currentClass = classQueue.removeLast();
Map<Class<?>, MatchableConverters> converters = getTargetConvertersForSource(currentClass);
GenericConverter converter = getMatchingConverterForTarget(sourceType, targetType, converters);
if (converter != null) {
return converter;
}
Class<?>[] interfaces = currentClass.getInterfaces();
for (Class<?> ifc : interfaces) {
classQueue.addFirst(ifc);
}
}
Map<Class<?>, MatchableConverters> objectConverters = getTargetConvertersForSource(Object.class);
return getMatchingConverterForTarget(sourceType, targetType, objectConverters);
}
else if (sourceObjectType.isArray()) {
LinkedList<Class<?>> classQueue = new LinkedList<Class<?>>();
classQueue.addFirst(sourceObjectType);
while (!classQueue.isEmpty()) {
Class<?> currentClass = classQueue.removeLast();
Map<Class<?>, MatchableConverters> converters = getTargetConvertersForSource(currentClass);
GenericConverter converter = getMatchingConverterForTarget(sourceType, targetType, converters);
if (converter != null) {
return converter;
}
Class<?> componentType = ClassUtils.resolvePrimitiveIfNecessary(currentClass.getComponentType());
if (componentType.getSuperclass() != null) {
classQueue.addFirst(Array.newInstance(componentType.getSuperclass(), 0).getClass());
}
else if (componentType.isInterface()) {
classQueue.addFirst(Object[].class);
}
}
return null;
}
else {
HashSet<Class<?>> interfaces = new LinkedHashSet<Class<?>>();
LinkedList<Class<?>> classQueue = new LinkedList<Class<?>>();
classQueue.addFirst(sourceObjectType);
while (!classQueue.isEmpty()) {
Class<?> currentClass = classQueue.removeLast();
Map<Class<?>, MatchableConverters> converters = getTargetConvertersForSource(currentClass);
GenericConverter converter = getMatchingConverterForTarget(sourceType, targetType, converters);
if (converter != null) {
return converter;
}
Class<?> superClass = currentClass.getSuperclass();
if (superClass != null && superClass != Object.class) {
classQueue.addFirst(superClass);
}
for (Class<?> interfaceType : currentClass.getInterfaces()) {
addInterfaceHierarchy(interfaceType, interfaces);
}
}
for (Class<?> interfaceType : interfaces) {
Map<Class<?>, MatchableConverters> converters = getTargetConvertersForSource(interfaceType);
GenericConverter converter = getMatchingConverterForTarget(sourceType, targetType, converters);
if (converter != null) {
return converter;
}
}
Map<Class<?>, MatchableConverters> objectConverters = getTargetConvertersForSource(Object.class);
return getMatchingConverterForTarget(sourceType, targetType, objectConverters);
}
}
private Map<Class<?>, MatchableConverters> getTargetConvertersForSource(Class<?> sourceType) {
Map<Class<?>, MatchableConverters> converters = this.converters.get(sourceType);
if (converters == null) {
converters = Collections.emptyMap();
}
return converters;
}
private GenericConverter getMatchingConverterForTarget(TypeDescriptor sourceType, TypeDescriptor targetType,
Map<Class<?>, MatchableConverters> converters) {
Class<?> targetObjectType = targetType.getObjectType();
if (targetObjectType.isInterface()) {
LinkedList<Class<?>> classQueue = new LinkedList<Class<?>>();
classQueue.addFirst(targetObjectType);
while (!classQueue.isEmpty()) {
Class<?> currentClass = classQueue.removeLast();
MatchableConverters matchable = converters.get(currentClass);
GenericConverter converter = matchConverter(matchable, sourceType, targetType);
if (converter != null) {
return converter;
}
Class<?>[] interfaces = currentClass.getInterfaces();
for (Class<?> ifc : interfaces) {
classQueue.addFirst(ifc);
}
}
return matchConverter(converters.get(Object.class), sourceType, targetType);
}
else if (targetObjectType.isArray()) {
LinkedList<Class<?>> classQueue = new LinkedList<Class<?>>();
classQueue.addFirst(targetObjectType);
while (!classQueue.isEmpty()) {
Class<?> currentClass = classQueue.removeLast();
MatchableConverters matchable = converters.get(currentClass);
GenericConverter converter = matchConverter(matchable, sourceType, targetType);
if (converter != null) {
return converter;
}
Class<?> componentType = ClassUtils.resolvePrimitiveIfNecessary(currentClass.getComponentType());
if (componentType.getSuperclass() != null) {
classQueue.addFirst(Array.newInstance(componentType.getSuperclass(), 0).getClass());
}
else if (componentType.isInterface()) {
classQueue.addFirst(Object[].class);
}
}
return null;
}
else {
Set<Class<?>> interfaces = new LinkedHashSet<Class<?>>();
LinkedList<Class<?>> classQueue = new LinkedList<Class<?>>();
classQueue.addFirst(targetObjectType);
while (!classQueue.isEmpty()) {
Class<?> currentClass = classQueue.removeLast();
MatchableConverters matchable = converters.get(currentClass);
GenericConverter converter = matchConverter(matchable, sourceType, targetType);
if (converter != null) {
return converter;
}
Class<?> superClass = currentClass.getSuperclass();
if (superClass != null && superClass != Object.class) {
classQueue.addFirst(superClass);
}
for (Class<?> interfaceType : currentClass.getInterfaces()) {
addInterfaceHierarchy(interfaceType, interfaces);
}
}
for (Class<?> interfaceType : interfaces) {
MatchableConverters matchable = converters.get(interfaceType);
GenericConverter converter = matchConverter(matchable, sourceType, targetType);
if (converter != null) {
return converter;
}
}
return matchConverter(converters.get(Object.class), sourceType, targetType);
}
}
private void addInterfaceHierarchy(Class<?> interfaceType, Set<Class<?>> interfaces) {
interfaces.add(interfaceType);
for (Class<?> inheritedInterface : interfaceType.getInterfaces()) {
addInterfaceHierarchy(inheritedInterface, interfaces);
}
}
private GenericConverter matchConverter(
MatchableConverters matchable, TypeDescriptor sourceFieldType, TypeDescriptor targetFieldType) {
if (matchable == null) {
return null;
}
return matchable.matchConverter(sourceFieldType, targetFieldType);
}
private Object handleConverterNotFound(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
assertNotPrimitiveTargetType(sourceType, targetType);
return source;
}
else if (sourceType.isAssignableTo(targetType) && targetType.getObjectType().isInstance(source)) {
if (sourceType.isAssignableTo(targetType) && targetType.getObjectType().isInstance(source)) {
return source;
}
else {
throw new ConverterNotFoundException(sourceType, targetType);
}
throw new ConverterNotFoundException(sourceType, targetType);
}
private Object handleResult(TypeDescriptor sourceType, TypeDescriptor targetType, Object result) {
@ -482,6 +279,7 @@ public class GenericConversionService implements ConfigurableConversionService {
}
return result;
}
private void assertNotPrimitiveTargetType(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (targetType.isPrimitive()) {
throw new ConversionFailedException(sourceType, targetType, null,
@ -490,24 +288,35 @@ public class GenericConversionService implements ConfigurableConversionService {
}
/**
* Adapts a {@link Converter} to a {@link GenericConverter}.
*/
@SuppressWarnings("unchecked")
private final class ConverterAdapter implements GenericConverter {
private final class ConverterAdapter implements ConditionalGenericConverter {
private final ConvertiblePair typeInfo;
private final Converter<Object, Object> converter;
public ConverterAdapter(ConvertiblePair typeInfo, Converter<?, ?> converter) {
this.converter = (Converter<Object, Object>) converter;
this.typeInfo = typeInfo;
}
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(this.typeInfo);
}
public boolean matchesTargetType(TypeDescriptor targetType) {
return this.typeInfo.getTargetType().equals(targetType.getObjectType());
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if(!this.typeInfo.getTargetType().equals(targetType.getObjectType())) {
return false;
}
if (this.converter instanceof ConditionalConverter) {
return ((ConditionalConverter) this.converter).matches(sourceType, targetType);
}
return true;
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
@ -518,28 +327,48 @@ public class GenericConversionService implements ConfigurableConversionService {
}
public String toString() {
return this.typeInfo.getSourceType().getName() + " -> " + this.typeInfo.getTargetType().getName() +
" : " + this.converter.toString();
return this.typeInfo.getSourceType().getName() + " -> " +
this.typeInfo.getTargetType().getName() + " : " +
this.converter.toString();
}
}
/**
* Adapts a {@link ConverterFactory} to a {@link GenericConverter}.
*/
@SuppressWarnings("unchecked")
private final class ConverterFactoryAdapter implements GenericConverter {
private final class ConverterFactoryAdapter implements ConditionalGenericConverter {
private final ConvertiblePair typeInfo;
private final ConverterFactory<Object, Object> converterFactory;
public ConverterFactoryAdapter(ConvertiblePair typeInfo, ConverterFactory<?, ?> converterFactory) {
this.converterFactory = (ConverterFactory<Object, Object>) converterFactory;
this.typeInfo = typeInfo;
}
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(this.typeInfo);
}
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
boolean matches = true;
if (this.converterFactory instanceof ConditionalConverter) {
matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType);
}
if(matches) {
Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
if(converter instanceof ConditionalConverter) {
matches = ((ConditionalConverter) converter).matches(sourceType, targetType);
}
}
return matches;
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return convertNullSource(sourceType, targetType);
@ -548,79 +377,29 @@ public class GenericConversionService implements ConfigurableConversionService {
}
public String toString() {
return this.typeInfo.getSourceType().getName() + " -> " + this.typeInfo.getTargetType().getName() +
" : " + this.converterFactory.toString();
}
}
private static class MatchableConverters {
private LinkedList<ConditionalGenericConverter> conditionalConverters;
private GenericConverter defaultConverter;
public void add(GenericConverter converter) {
if (converter instanceof ConditionalGenericConverter) {
if (this.conditionalConverters == null) {
this.conditionalConverters = new LinkedList<ConditionalGenericConverter>();
}
this.conditionalConverters.addFirst((ConditionalGenericConverter) converter);
}
else {
this.defaultConverter = converter;
}
}
public GenericConverter matchConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (this.conditionalConverters != null) {
for (ConditionalGenericConverter conditional : this.conditionalConverters) {
if (conditional.matches(sourceType, targetType)) {
return conditional;
}
}
}
if (this.defaultConverter instanceof ConverterAdapter) {
ConverterAdapter adapter = (ConverterAdapter) this.defaultConverter;
if (!adapter.matchesTargetType(targetType)) {
return null;
}
}
return this.defaultConverter;
}
public String toString() {
if (this.conditionalConverters != null) {
StringBuilder builder = new StringBuilder();
for (Iterator<ConditionalGenericConverter> it = this.conditionalConverters.iterator(); it.hasNext();) {
builder.append(it.next());
if (it.hasNext()) {
builder.append(", ");
}
}
if (this.defaultConverter != null) {
builder.append(", ").append(this.defaultConverter);
}
return builder.toString();
}
else {
return this.defaultConverter.toString();
}
return this.typeInfo.getSourceType().getName() + " -> " +
this.typeInfo.getTargetType().getName() + " : " +
this.converterFactory.toString();
}
}
/**
* Key for use with the converter cache.
*/
private static final class ConverterCacheKey {
private final TypeDescriptor sourceType;
private final TypeDescriptor targetType;
public ConverterCacheKey(TypeDescriptor sourceType, TypeDescriptor targetType) {
this.sourceType = sourceType;
this.targetType = targetType;
}
public boolean equals(Object other) {
if (this == other) {
return true;
@ -629,16 +408,238 @@ public class GenericConversionService implements ConfigurableConversionService {
return false;
}
ConverterCacheKey otherKey = (ConverterCacheKey) other;
return this.sourceType.equals(otherKey.sourceType) && this.targetType.equals(otherKey.targetType);
return ObjectUtils.nullSafeEquals(this.sourceType, otherKey.sourceType)
&& ObjectUtils.nullSafeEquals(this.targetType, otherKey.targetType);
}
public int hashCode() {
return this.sourceType.hashCode() * 29 + this.targetType.hashCode();
return ObjectUtils.nullSafeHashCode(this.sourceType) * 29
+ ObjectUtils.nullSafeHashCode(this.targetType);
}
public String toString() {
return "ConverterCacheKey [sourceType = " + this.sourceType + ", targetType = " + this.targetType + "]";
return "ConverterCacheKey [sourceType = " + this.sourceType
+ ", targetType = " + this.targetType + "]";
}
}
/**
* Manages all converters registered with the service.
*/
private static class Converters {
private static final Set<Class<?>> IGNORED_CLASSES;
static {
Set<Class<?>> ignored = new HashSet<Class<?>>();
ignored.add(Object.class);
ignored.add(Object[].class);
IGNORED_CLASSES = Collections.unmodifiableSet(ignored);
}
private final Set<GenericConverter> globalConverters =
new LinkedHashSet<GenericConverter>();
private final Map<ConvertiblePair, ConvertersForPair> converters =
new LinkedHashMap<ConvertiblePair, ConvertersForPair>(36);
public void add(GenericConverter converter) {
Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
if (convertibleTypes == null) {
Assert.state(converter instanceof ConditionalConverter,
"Only conditional converters may return null convertible types");
globalConverters.add(converter);
} else {
for (ConvertiblePair convertiblePair : convertibleTypes) {
ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair);
convertersForPair.add(converter);
}
}
}
private ConvertersForPair getMatchableConverters(ConvertiblePair convertiblePair) {
ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
if (convertersForPair == null) {
convertersForPair = new ConvertersForPair();
this.converters.put(convertiblePair, convertersForPair);
}
return convertersForPair;
}
public void remove(Class<?> sourceType, Class<?> targetType) {
converters.remove(new ConvertiblePair(sourceType, targetType));
}
/**
* Find a {@link GenericConverter} given a source and target type. This method will
* attempt to match all possible converters by working though the class and interface
* hierarchy of the types.
* @param sourceType the source type
* @param targetType the target type
* @return a {@link GenericConverter} or <tt>null</tt>
* @see #getTypeHierarchy(Class)
*/
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Search the full type hierarchy
List<TypeDescriptor> sourceCandidates = getTypeHierarchy(sourceType);
List<TypeDescriptor> targetCandidates = getTypeHierarchy(targetType);
for (TypeDescriptor sourceCandidate : sourceCandidates) {
for (TypeDescriptor targetCandidate : targetCandidates) {
GenericConverter converter = getRegisteredConverter(
sourceType, targetType, sourceCandidate, targetCandidate);
if(converter != null) {
return converter;
}
}
}
return null;
}
private GenericConverter getRegisteredConverter(TypeDescriptor sourceType, TypeDescriptor targetType,
TypeDescriptor sourceCandidate, TypeDescriptor targetCandidate) {
// Check specifically registered converters
ConvertersForPair convertersForPair = converters.get(new ConvertiblePair(
sourceCandidate.getType(), targetCandidate.getType()));
GenericConverter converter = convertersForPair == null ? null
: convertersForPair.getConverter(sourceType, targetType);
if (converter != null) {
return converter;
}
// Check ConditionalGenericConverter that match all types
for (GenericConverter globalConverter : this.globalConverters) {
if (((ConditionalConverter)globalConverter).matches(
sourceCandidate, targetCandidate)) {
return globalConverter;
}
}
return null;
}
/**
* Returns an ordered class hierarchy for the given type.
* @param type the type
* @return an ordered list of all classes that the given type extends or
* implements.
*/
private List<TypeDescriptor> getTypeHierarchy(TypeDescriptor type) {
if(type.isPrimitive()) {
type = TypeDescriptor.valueOf(type.getObjectType());
}
Set<TypeDescriptor> typeHierarchy = new LinkedHashSet<TypeDescriptor>();
collectTypeHierarchy(typeHierarchy, type);
if(type.isArray()) {
typeHierarchy.add(TypeDescriptor.valueOf(Object[].class));
}
typeHierarchy.add(TypeDescriptor.valueOf(Object.class));
return new ArrayList<TypeDescriptor>(typeHierarchy);
}
private void collectTypeHierarchy(Set<TypeDescriptor> typeHierarchy,
TypeDescriptor type) {
if(type != null && !IGNORED_CLASSES.contains(type.getType())) {
if(typeHierarchy.add(type)) {
Class<?> superclass = type.getType().getSuperclass();
if (type.isArray()) {
superclass = ClassUtils.resolvePrimitiveIfNecessary(superclass);
}
collectTypeHierarchy(typeHierarchy, createRelated(type, superclass));
for (Class<?> implementsInterface : type.getType().getInterfaces()) {
collectTypeHierarchy(typeHierarchy, createRelated(type, implementsInterface));
}
}
}
}
private TypeDescriptor createRelated(TypeDescriptor type, Class<?> relatedType) {
if (relatedType == null && type.isArray()) {
relatedType = Array.newInstance(relatedType, 0).getClass();
}
if(!type.getType().equals(relatedType)) {
return type.upcast(relatedType);
}
return null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ConversionService converters = ").append("\n");
for (String converterString : getConverterStrings()) {
builder.append("\t");
builder.append(converterString);
builder.append("\n");
}
return builder.toString();
}
private List<String> getConverterStrings() {
List<String> converterStrings = new ArrayList<String>();
for (ConvertersForPair convertersForPair : converters.values()) {
converterStrings.add(convertersForPair.toString());
}
Collections.sort(converterStrings);
return converterStrings;
}
}
/**
* Manages converters registered with a specific {@link ConvertiblePair}.
*/
private static class ConvertersForPair {
private final LinkedList<GenericConverter> converters = new LinkedList<GenericConverter>();
public void add(GenericConverter converter) {
this.converters.addFirst(converter);
}
public GenericConverter getConverter(TypeDescriptor sourceType,
TypeDescriptor targetType) {
for (GenericConverter converter : this.converters) {
if (!(converter instanceof ConditionalGenericConverter)
|| ((ConditionalGenericConverter) converter).matches(sourceType,
targetType)) {
return converter;
}
}
return null;
}
public String toString() {
return StringUtils.collectionToCommaDelimitedString(this.converters);
}
}
/**
* Internal converter that performs no operation.
*/
private static class NoOpConverter implements GenericConverter {
private String name;
public NoOpConverter(String name) {
this.name = name;
}
public Set<ConvertiblePair> getConvertibleTypes() {
return null;
}
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
return source;
}
@Override
public String toString() {
return name;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2012 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.
@ -16,6 +16,8 @@
package org.springframework.core.convert.support;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.util.NumberUtils;
@ -38,12 +40,17 @@ import org.springframework.util.NumberUtils;
* @see java.math.BigDecimal
* @see NumberUtils
*/
final class NumberToNumberConverterFactory implements ConverterFactory<Number, Number> {
final class NumberToNumberConverterFactory implements ConverterFactory<Number, Number>,
ConditionalConverter {
public <T extends Number> Converter<Number, T> getConverter(Class<T> targetType) {
return new NumberToNumber<T>(targetType);
}
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return !sourceType.equals(targetType);
}
private final static class NumberToNumber<T extends Number> implements Converter<Number, T> {
private final Class<T> targetType;

View File

@ -28,14 +28,9 @@ import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.*;
/**
* @author Keith Donald
@ -801,4 +796,26 @@ public class TypeDescriptorTests {
public Map<CharSequence, Number> isAssignableMapKeyValueTypes;
@Test
public void testUpCast() throws Exception {
Property property = new Property(getClass(), getClass().getMethod("getProperty"),
getClass().getMethod("setProperty", Map.class));
TypeDescriptor typeDescriptor = new TypeDescriptor(property);
TypeDescriptor upCast = typeDescriptor.upcast(Object.class);
assertTrue(upCast.getAnnotation(MethodAnnotation1.class) != null);
}
@Test
public void testUpCastNotSuper() throws Exception {
Property property = new Property(getClass(), getClass().getMethod("getProperty"),
getClass().getMethod("setProperty", Map.class));
TypeDescriptor typeDescriptor = new TypeDescriptor(property);
try {
typeDescriptor.upcast(Collection.class);
fail("Did not throw");
} catch(IllegalArgumentException e) {
assertEquals("interface java.util.Map is not assignable to interface java.util.Collection", e.getMessage());
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@ -16,14 +16,6 @@
package org.springframework.core.convert.support;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import java.awt.Color;
import java.awt.SystemColor;
import java.util.ArrayList;
@ -31,7 +23,9 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -39,19 +33,25 @@ import java.util.Set;
import java.util.UUID;
import org.junit.Test;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.io.DescriptiveResource;
import org.springframework.core.io.Resource;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;
import static org.junit.Assert.*;
/**
* @author Keith Donald
* @author Juergen Hoeller
* @author Phillip Webb
*/
public class GenericConversionServiceTests {
@ -599,4 +599,236 @@ public class GenericConversionServiceTests {
assertFalse(pair.hashCode() == pairOpposite.hashCode());
}
@Test
public void convertPrimitiveArray() throws Exception {
GenericConversionService conversionService = new DefaultConversionService();
byte[] byteArray = new byte[] { 1, 2, 3 };
Byte[] converted = conversionService.convert(byteArray, Byte[].class);
assertTrue(Arrays.equals(converted, new Byte[] { 1, 2, 3 }));
}
@Test
public void canConvertIllegalArgumentNullTargetTypeFromClass() {
try {
conversionService.canConvert(String.class, null);
fail("Did not thow IllegalArgumentException");
} catch(IllegalArgumentException e) {
}
}
@Test
public void canConvertIllegalArgumentNullTargetTypeFromTypeDescriptor() {
try {
conversionService.canConvert(TypeDescriptor.valueOf(String.class), null);
fail("Did not thow IllegalArgumentException");
} catch(IllegalArgumentException e) {
}
}
@Test
@SuppressWarnings({ "rawtypes" })
public void convertHashMapValuesToList() throws Exception {
GenericConversionService conversionService = new DefaultConversionService();
Map<String, Integer> hashMap = new LinkedHashMap<String, Integer>();
hashMap.put("1", 1);
hashMap.put("2", 2);
List converted = conversionService.convert(hashMap.values(), List.class);
assertEquals(Arrays.asList(1, 2), converted);
}
@Test
public void removeConvertible() throws Exception {
conversionService.addConverter(new ColorConverter());
assertTrue(conversionService.canConvert(String.class, Color.class));
conversionService.removeConvertible(String.class, Color.class);
assertFalse(conversionService.canConvert(String.class, Color.class));
}
@Test
public void conditionalConverter() throws Exception {
GenericConversionService conversionService = new GenericConversionService();
MyConditionalConverter converter = new MyConditionalConverter();
conversionService.addConverter(new ColorConverter());
conversionService.addConverter(converter);
assertEquals(Color.BLACK, conversionService.convert("#000000", Color.class));
assertTrue(converter.getMatchAttempts() > 0);
}
@Test
public void conditionalConverterFactory() throws Exception {
GenericConversionService conversionService = new GenericConversionService();
MyConditionalConverterFactory converter = new MyConditionalConverterFactory();
conversionService.addConverter(new ColorConverter());
conversionService.addConverterFactory(converter);
assertEquals(Color.BLACK, conversionService.convert("#000000", Color.class));
assertTrue(converter.getMatchAttempts() > 0);
assertTrue(converter.getNestedMatchAttempts() > 0);
}
@Test
public void shouldNotSuportNullConvertibleTypesFromNonConditionalGenericConverter()
throws Exception {
GenericConversionService conversionService = new GenericConversionService();
GenericConverter converter = new GenericConverter() {
public Set<ConvertiblePair> getConvertibleTypes() {
return null;
}
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
return null;
}
};
try {
conversionService.addConverter(converter);
fail("Did not throw");
} catch (IllegalStateException e) {
assertEquals("Only conditional converters may return null convertible types", e.getMessage());
}
}
@Test
public void conditionalConversionForAllTypes() throws Exception {
GenericConversionService conversionService = new GenericConversionService();
MyConditionalGenericConverter converter = new MyConditionalGenericConverter();
conversionService.addConverter(converter);
assertEquals((Integer) 3, conversionService.convert(3, Integer.class));
Iterator<TypeDescriptor> iterator = converter.getSourceTypes().iterator();
assertEquals(Integer.class, iterator.next().getType());
assertEquals(Number.class, iterator.next().getType());
TypeDescriptor last = null;
while (iterator.hasNext()) {
last = iterator.next();
}
assertEquals(Object.class, last.getType());
}
@Test
public void convertOptimizeArray() throws Exception {
// SPR-9566
GenericConversionService conversionService = new DefaultConversionService();
byte[] byteArray = new byte[] { 1, 2, 3 };
byte[] converted = conversionService.convert(byteArray, byte[].class);
assertSame(byteArray, converted);
}
@Test
public void convertCannotOptimizeArray() throws Exception {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(new Converter<Byte, Byte>() {
public Byte convert(Byte source) {
return (byte) (source + 1);
}
});
DefaultConversionService.addDefaultConverters(conversionService);
byte[] byteArray = new byte[] { 1, 2, 3 };
byte[] converted = conversionService.convert(byteArray, byte[].class);
assertNotSame(byteArray, converted);
assertTrue(Arrays.equals(new byte[] { 2, 3, 4 }, converted));
}
@Test
public void testEnumToStringConversion() {
conversionService.addConverter(new EnumToStringConverter(conversionService));
String result = conversionService.convert(MyEnum.A, String.class);
assertEquals("A", result);
}
@Test
public void testEnumWithInterfaceToStringConversion() {
// SPR-9692
conversionService.addConverter(new EnumToStringConverter(conversionService));
conversionService.addConverter(new MyEnumInterfaceToStringConverter<MyEnum>());
String result = conversionService.convert(MyEnum.A, String.class);
assertEquals("1", result);
}
private static class MyConditionalConverter implements Converter<String, Color>,
ConditionalConverter {
private int matchAttempts = 0;
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
matchAttempts++;
return false;
}
public Color convert(String source) {
throw new IllegalStateException();
}
public int getMatchAttempts() {
return matchAttempts;
}
}
private static class MyConditionalGenericConverter implements GenericConverter,
ConditionalConverter {
private Set<TypeDescriptor> sourceTypes = new LinkedHashSet<TypeDescriptor>();
public Set<ConvertiblePair> getConvertibleTypes() {
return null;
}
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
sourceTypes.add(sourceType);
return false;
}
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
return null;
}
public Set<TypeDescriptor> getSourceTypes() {
return sourceTypes;
}
}
private static class MyConditionalConverterFactory implements
ConverterFactory<String, Color>, ConditionalConverter {
private MyConditionalConverter converter = new MyConditionalConverter();
private int matchAttempts = 0;
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
matchAttempts++;
return true;
}
@SuppressWarnings("unchecked")
public <T extends Color> Converter<String, T> getConverter(Class<T> targetType) {
return (Converter<String, T>) converter;
}
public int getMatchAttempts() {
return matchAttempts;
}
public int getNestedMatchAttempts() {
return converter.getMatchAttempts();
}
}
interface MyEnumInterface {
String getCode();
}
public static enum MyEnum implements MyEnumInterface {
A {
public String getCode() {
return "1";
}
};
}
private static class MyEnumInterfaceToStringConverter<T extends MyEnumInterface>
implements Converter<T, String> {
public String convert(T source) {
return source.getCode();
}
}
}

View File

@ -16,7 +16,8 @@
package org.springframework.expression.spel;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
@ -29,6 +30,7 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.AccessException;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionInvocationTargetException;
@ -44,6 +46,7 @@ import org.springframework.expression.spel.testresources.PlaceOfBirth;
* Tests invocation of methods.
*
* @author Andy Clement
* @author Phillip Webb
*/
public class MethodInvocationTests extends ExpressionTestCase {
@ -369,4 +372,30 @@ public class MethodInvocationTests extends ExpressionTestCase {
Object value = expression.getValue(new StandardEvaluationContext(String.class));
assertEquals(value, "java.lang.String");
}
@Test
public void invokeMethodWithoutConversion() throws Exception {
final BytesService service = new BytesService();
byte[] bytes = new byte[100];
StandardEvaluationContext context = new StandardEvaluationContext(bytes);
context.setBeanResolver(new BeanResolver() {
public Object resolve(EvaluationContext context, String beanName)
throws AccessException {
if ("service".equals(beanName)) {
return service;
}
return null;
}
});
Expression expression = parser.parseExpression("@service.handleBytes(#root)");
byte[] outBytes = expression.getValue(context, byte[].class);
assertSame(bytes, outBytes);
}
public static class BytesService {
public byte[] handleBytes(byte[] bytes) {
return bytes;
}
}
}