diff --git a/spring-core/src/main/java/org/springframework/core/convert/AbstractDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/AbstractDescriptor.java deleted file mode 100644 index 86995f3262..0000000000 --- a/spring-core/src/main/java/org/springframework/core/convert/AbstractDescriptor.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2002-2011 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; - -import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.Map; - -import org.springframework.util.Assert; - -/** - * @author Keith Donald - * @since 3.1 - */ -abstract class AbstractDescriptor { - - private final Class type; - - - protected AbstractDescriptor(Class type) { - Assert.notNull(type, "Type must not be null"); - this.type = type; - } - - - public Class getType() { - return this.type; - } - - public TypeDescriptor getElementTypeDescriptor() { - if (isCollection()) { - Class elementType = resolveCollectionElementType(); - return (elementType != null ? new TypeDescriptor(nested(elementType, 0)) : null); - } - else if (isArray()) { - Class elementType = getType().getComponentType(); - return new TypeDescriptor(nested(elementType, 0)); - } - else { - return null; - } - } - - public TypeDescriptor getMapKeyTypeDescriptor() { - if (isMap()) { - Class keyType = resolveMapKeyType(); - return keyType != null ? new TypeDescriptor(nested(keyType, 0)) : null; - } - else { - return null; - } - } - - public TypeDescriptor getMapValueTypeDescriptor() { - if (isMap()) { - Class valueType = resolveMapValueType(); - return valueType != null ? new TypeDescriptor(nested(valueType, 1)) : null; - } - else { - return null; - } - } - - public AbstractDescriptor nested() { - if (isCollection()) { - Class elementType = resolveCollectionElementType(); - return (elementType != null ? nested(elementType, 0) : null); - } - else if (isArray()) { - return nested(getType().getComponentType(), 0); - } - else if (isMap()) { - Class mapValueType = resolveMapValueType(); - return (mapValueType != null ? nested(mapValueType, 1) : null); - } - else if (Object.class.equals(getType())) { - // could be a collection type but we don't know about its element type, - // so let's just assume there is an element type of type Object - return this; - } - else { - throw new IllegalStateException("Not a collection, array, or map: cannot resolve nested value types"); - } - } - - - // subclassing hooks - - public abstract Annotation[] getAnnotations(); - - protected abstract Class resolveCollectionElementType(); - - protected abstract Class resolveMapKeyType(); - - protected abstract Class resolveMapValueType(); - - protected abstract AbstractDescriptor nested(Class type, int typeIndex); - - - // internal helpers - - private boolean isCollection() { - return Collection.class.isAssignableFrom(getType()); - } - - private boolean isArray() { - return getType().isArray(); - } - - private boolean isMap() { - return Map.class.isAssignableFrom(getType()); - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/convert/BeanPropertyDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/BeanPropertyDescriptor.java deleted file mode 100644 index 8be6b0e2c6..0000000000 --- a/spring-core/src/main/java/org/springframework/core/convert/BeanPropertyDescriptor.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2002-2011 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; - -import java.lang.annotation.Annotation; - -import org.springframework.core.GenericCollectionTypeResolver; -import org.springframework.core.MethodParameter; - -/** - * @author Keith Donald - * @since 3.1 - */ -class BeanPropertyDescriptor extends AbstractDescriptor { - - private final Property property; - - private final MethodParameter methodParameter; - - private final Annotation[] annotations; - - - public BeanPropertyDescriptor(Property property) { - super(property.getType()); - this.property = property; - this.methodParameter = property.getMethodParameter(); - this.annotations = property.getAnnotations(); - } - - - @Override - public Annotation[] getAnnotations() { - return this.annotations; - } - - @Override - protected Class resolveCollectionElementType() { - return GenericCollectionTypeResolver.getCollectionParameterType(this.methodParameter); - } - - @Override - protected Class resolveMapKeyType() { - return GenericCollectionTypeResolver.getMapKeyParameterType(this.methodParameter); - } - - @Override - protected Class resolveMapValueType() { - return GenericCollectionTypeResolver.getMapValueParameterType(this.methodParameter); - } - - @Override - protected AbstractDescriptor nested(Class type, int typeIndex) { - MethodParameter methodParameter = new MethodParameter(this.methodParameter); - methodParameter.increaseNestingLevel(); - methodParameter.setTypeIndexForCurrentLevel(typeIndex); - return new BeanPropertyDescriptor(type, this.property, methodParameter, this.annotations); - } - - - // internal - - private BeanPropertyDescriptor(Class type, Property propertyDescriptor, MethodParameter methodParameter, Annotation[] annotations) { - super(type); - this.property = propertyDescriptor; - this.methodParameter = methodParameter; - this.annotations = annotations; - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ClassDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/ClassDescriptor.java deleted file mode 100644 index 87f8e06629..0000000000 --- a/spring-core/src/main/java/org/springframework/core/convert/ClassDescriptor.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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; - -import java.lang.annotation.Annotation; - -import org.springframework.core.GenericCollectionTypeResolver; - -/** - * @author Keith Donald - * @author Phillip Webb - * @since 3.1 - */ -class ClassDescriptor extends AbstractDescriptor { - - ClassDescriptor(Class type) { - super(type); - } - - @Override - public Annotation[] getAnnotations() { - return TypeDescriptor.EMPTY_ANNOTATION_ARRAY; - } - - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - protected Class resolveCollectionElementType() { - return GenericCollectionTypeResolver.getCollectionType((Class) getType()); - } - - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - protected Class resolveMapKeyType() { - return GenericCollectionTypeResolver.getMapKeyType((Class) getType()); - } - - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - protected Class resolveMapValueType() { - return GenericCollectionTypeResolver.getMapValueType((Class) getType()); - } - - @Override - protected AbstractDescriptor nested(Class type, int typeIndex) { - return new ClassDescriptor(type); - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/convert/FieldDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/FieldDescriptor.java deleted file mode 100644 index 9d951bb312..0000000000 --- a/spring-core/src/main/java/org/springframework/core/convert/FieldDescriptor.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2002-2011 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; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.core.GenericCollectionTypeResolver; - -/** - * @author Keith Donald - * @since 3.1 - */ -class FieldDescriptor extends AbstractDescriptor { - - private final Field field; - - private final int nestingLevel; - - private Map typeIndexesPerLevel; - - - public FieldDescriptor(Field field) { - super(field.getType()); - this.field = field; - this.nestingLevel = 1; - } - - private FieldDescriptor(Class type, Field field, int nestingLevel, int typeIndex, Map typeIndexesPerLevel) { - super(type); - this.field = field; - this.nestingLevel = nestingLevel; - this.typeIndexesPerLevel = typeIndexesPerLevel; - this.typeIndexesPerLevel.put(nestingLevel, typeIndex); - } - - - @Override - public Annotation[] getAnnotations() { - return TypeDescriptor.nullSafeAnnotations(this.field.getAnnotations()); - } - - @Override - protected Class resolveCollectionElementType() { - return GenericCollectionTypeResolver.getCollectionFieldType(this.field, this.nestingLevel, this.typeIndexesPerLevel); - } - - @Override - protected Class resolveMapKeyType() { - return GenericCollectionTypeResolver.getMapKeyFieldType(this.field, this.nestingLevel, this.typeIndexesPerLevel); - } - - @Override - protected Class resolveMapValueType() { - return GenericCollectionTypeResolver.getMapValueFieldType(this.field, this.nestingLevel, this.typeIndexesPerLevel); - } - - @Override - protected AbstractDescriptor nested(Class type, int typeIndex) { - if (this.typeIndexesPerLevel == null) { - this.typeIndexesPerLevel = new HashMap(4); - } - return new FieldDescriptor(type, this.field, this.nestingLevel + 1, typeIndex, this.typeIndexesPerLevel); - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ParameterDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/ParameterDescriptor.java deleted file mode 100644 index 773464c8a1..0000000000 --- a/spring-core/src/main/java/org/springframework/core/convert/ParameterDescriptor.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2002-2011 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; - -import java.lang.annotation.Annotation; - -import org.springframework.core.GenericCollectionTypeResolver; -import org.springframework.core.MethodParameter; - -/** - * @author Keith Donald - * @since 3.1 - */ -class ParameterDescriptor extends AbstractDescriptor { - - private final MethodParameter methodParameter; - - - public ParameterDescriptor(MethodParameter methodParameter) { - super(methodParameter.getParameterType()); - if (methodParameter.getNestingLevel() != 1) { - throw new IllegalArgumentException("MethodParameter argument must have its nestingLevel set to 1"); - } - this.methodParameter = methodParameter; - } - - private ParameterDescriptor(Class type, MethodParameter methodParameter) { - super(type); - this.methodParameter = methodParameter; - } - - - @Override - public Annotation[] getAnnotations() { - if (this.methodParameter.getParameterIndex() == -1) { - return TypeDescriptor.nullSafeAnnotations(this.methodParameter.getMethodAnnotations()); - } - else { - return TypeDescriptor.nullSafeAnnotations(this.methodParameter.getParameterAnnotations()); - } - } - - @Override - protected Class resolveCollectionElementType() { - return GenericCollectionTypeResolver.getCollectionParameterType(this.methodParameter); - } - - @Override - protected Class resolveMapKeyType() { - return GenericCollectionTypeResolver.getMapKeyParameterType(this.methodParameter); - } - - @Override - protected Class resolveMapValueType() { - return GenericCollectionTypeResolver.getMapValueParameterType(this.methodParameter); - } - - @Override - protected AbstractDescriptor nested(Class type, int typeIndex) { - MethodParameter methodParameter = new MethodParameter(this.methodParameter); - methodParameter.increaseNestingLevel(); - methodParameter.setTypeIndexForCurrentLevel(typeIndex); - return new ParameterDescriptor(type, methodParameter); - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 9a3b2ad413..96d6c97538 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -18,13 +18,13 @@ package org.springframework.core.convert; import java.io.Serializable; import java.lang.annotation.Annotation; -import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -46,36 +46,22 @@ public class TypeDescriptor implements Serializable { static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; - private static final Map, TypeDescriptor> typeDescriptorCache = new HashMap, TypeDescriptor>(); + private static final Map, TypeDescriptor> commonTypesCache = new HashMap, TypeDescriptor>(); + private static final Class[] CACHED_COMMON_TYPES = { Boolean.class, byte.class, + Byte.class, char.class, Character.class, short.class, Short.class, int.class, + Integer.class, long.class, Long.class, float.class, Float.class, double.class, + Double.class, String.class }; static { - typeDescriptorCache.put(boolean.class, new TypeDescriptor(boolean.class)); - typeDescriptorCache.put(Boolean.class, new TypeDescriptor(Boolean.class)); - typeDescriptorCache.put(byte.class, new TypeDescriptor(byte.class)); - typeDescriptorCache.put(Byte.class, new TypeDescriptor(Byte.class)); - typeDescriptorCache.put(char.class, new TypeDescriptor(char.class)); - typeDescriptorCache.put(Character.class, new TypeDescriptor(Character.class)); - typeDescriptorCache.put(short.class, new TypeDescriptor(short.class)); - typeDescriptorCache.put(Short.class, new TypeDescriptor(Short.class)); - typeDescriptorCache.put(int.class, new TypeDescriptor(int.class)); - typeDescriptorCache.put(Integer.class, new TypeDescriptor(Integer.class)); - typeDescriptorCache.put(long.class, new TypeDescriptor(long.class)); - typeDescriptorCache.put(Long.class, new TypeDescriptor(Long.class)); - typeDescriptorCache.put(float.class, new TypeDescriptor(float.class)); - typeDescriptorCache.put(Float.class, new TypeDescriptor(Float.class)); - typeDescriptorCache.put(double.class, new TypeDescriptor(double.class)); - typeDescriptorCache.put(Double.class, new TypeDescriptor(Double.class)); - typeDescriptorCache.put(String.class, new TypeDescriptor(String.class)); + for (Class preCachedClass : CACHED_COMMON_TYPES) { + commonTypesCache.put(preCachedClass, valueOf(preCachedClass)); + } } private final Class type; - private final TypeDescriptor elementTypeDescriptor; - - private final TypeDescriptor mapKeyTypeDescriptor; - - private final TypeDescriptor mapValueTypeDescriptor; + private final ResolvableType resolvableType; private final Annotation[] annotations; @@ -87,7 +73,15 @@ public class TypeDescriptor implements Serializable { * @param methodParameter the method parameter */ public TypeDescriptor(MethodParameter methodParameter) { - this(new ParameterDescriptor(methodParameter)); + Assert.notNull(methodParameter, "MethodParameter must not be null"); + if (methodParameter.getNestingLevel() != 1) { + throw new IllegalArgumentException("MethodParameter argument must have its nestingLevel set to 1"); + } + this.resolvableType = ResolvableType.forMethodParameter(methodParameter); + this.type = this.resolvableType.resolve(Object.class); + this.annotations = (methodParameter.getParameterIndex() == -1 ? + nullSafeAnnotations(methodParameter.getMethodAnnotations()) : + nullSafeAnnotations(methodParameter.getParameterAnnotations())); } /** @@ -96,7 +90,10 @@ public class TypeDescriptor implements Serializable { * @param field the field */ public TypeDescriptor(Field field) { - this(new FieldDescriptor(field)); + Assert.notNull(field, "Field must not be null"); + this.resolvableType = ResolvableType.forField(field); + this.type = this.resolvableType.resolve(Object.class); + this.annotations = nullSafeAnnotations(field.getAnnotations()); } /** @@ -106,165 +103,30 @@ public class TypeDescriptor implements Serializable { * @param property the property */ public TypeDescriptor(Property property) { - this(new BeanPropertyDescriptor(property)); + Assert.notNull(property, "Property must not be null"); + this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter()); + this.type = this.resolvableType.resolve(Object.class); + this.annotations = nullSafeAnnotations(property.getAnnotations()); + } + + private TypeDescriptor(ResolvableType resolvableType, Class type, Annotation[] annotations) { + this.resolvableType = resolvableType; + this.type = (type != null ? type : resolvableType.resolve(Object.class)); + this.annotations = nullSafeAnnotations(annotations); } - /** - * Create a new type descriptor from the given type. - *

Use this to instruct the conversion system to convert an object to a - * specific target type, when no type location such as a method parameter or - * field is available to provide additional conversion context. - *

Generally prefer use of {@link #forObject(Object)} for constructing type - * descriptors from source objects, as it handles the {@code null} object case. - * @param type the class - * @return the type descriptor - */ - public static TypeDescriptor valueOf(Class type) { - TypeDescriptor desc = typeDescriptorCache.get(type); - return (desc != null ? desc : new TypeDescriptor(type)); + private Annotation[] nullSafeAnnotations(Annotation[] annotations) { + return annotations != null ? annotations : EMPTY_ANNOTATION_ARRAY; } /** - * Create a new type descriptor from a {@link java.util.Collection} type. - *

Useful for converting to typed Collections. - *

For example, a {@code List} could be converted to a - * {@code List} by converting to a targetType built with this method. - * The method call to construct such a {@code TypeDescriptor} would look something - * like: {@code collection(List.class, TypeDescriptor.valueOf(EmailAddress.class));} - * @param collectionType the collection type, which must implement {@link Collection}. - * @param elementTypeDescriptor a descriptor for the collection's element type, - * used to convert collection elements - * @return the collection type descriptor + * Variation of {@link #getType()} that accounts for a primitive type by returning its object wrapper type. + *

This is useful for conversion service implementations that wish to normalize to object-based types + * and not work with primitive types directly. */ - public static TypeDescriptor collection(Class collectionType, TypeDescriptor elementTypeDescriptor) { - if (!Collection.class.isAssignableFrom(collectionType)) { - throw new IllegalArgumentException("collectionType must be a java.util.Collection"); - } - return new TypeDescriptor(collectionType, elementTypeDescriptor); - } - - /** - * Create a new type descriptor from a {@link java.util.Map} type. - *

Useful for converting to typed Maps. - *

For example, a Map<String, String> could be converted to a Map<Id, EmailAddress> by converting to a targetType built with this method: - * The method call to construct such a TypeDescriptor would look something like: map(Map.class, TypeDescriptor.valueOf(Id.class), TypeDescriptor.valueOf(EmailAddress.class)); - * @param mapType the map type, which must implement {@link Map} - * @param keyTypeDescriptor a descriptor for the map's key type, used to convert map keys - * @param valueTypeDescriptor the map's value type, used to convert map values - * @return the map type descriptor - */ - public static TypeDescriptor map(Class mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { - if (!Map.class.isAssignableFrom(mapType)) { - throw new IllegalArgumentException("mapType must be a java.util.Map"); - } - return new TypeDescriptor(mapType, keyTypeDescriptor, valueTypeDescriptor); - } - - /** - * Create a new type descriptor as an array of the specified type. - *

For example to create a {@code Map[]} use - * {@code TypeDescriptor.array(TypeDescriptor.map(Map.class, TypeDescriptor.value(String.class), TypeDescriptor.value(String.class)))}. - * @param elementTypeDescriptor the {@link TypeDescriptor} of the array element or {@code null} - * @return an array {@link TypeDescriptor} or {@code null} if {@code elementTypeDescriptor} is {@code null} - * @since 3.2.1 - */ - public static TypeDescriptor array(TypeDescriptor elementTypeDescriptor) { - if(elementTypeDescriptor == null) { - return null; - } - Class type = Array.newInstance(elementTypeDescriptor.getType(), 0).getClass(); - return new TypeDescriptor(type, elementTypeDescriptor, null, null, elementTypeDescriptor.getAnnotations()); - } - - /** - * Creates a type descriptor for a nested type declared within the method parameter. - *

For example, if the methodParameter is a {@code List} and the - * nesting level is 1, the nested type descriptor will be String.class. - *

If the methodParameter is a {@code List>} and the nesting - * level is 2, the nested type descriptor will also be a String.class. - *

If the methodParameter is a {@code Map} and the nesting - * level is 1, the nested type descriptor will be String, derived from the map value. - *

If the methodParameter is a {@code List>} and the - * nesting level is 2, the nested type descriptor will be String, derived from the map value. - *

Returns {@code null} if a nested type cannot be obtained because it was not declared. - * For example, if the method parameter is a {@code List}, the nested type - * descriptor returned will be {@code null}. - * @param methodParameter the method parameter with a nestingLevel of 1 - * @param nestingLevel the nesting level of the collection/array element or - * map key/value declaration within the method parameter - * @return the nested type descriptor at the specified nesting level, or null - * if it could not be obtained - * @throws IllegalArgumentException if the nesting level of the input - * {@link MethodParameter} argument is not 1 - * @throws IllegalArgumentException if the types up to the specified nesting - * level are not of collection, array, or map types - */ - public static TypeDescriptor nested(MethodParameter methodParameter, int nestingLevel) { - if (methodParameter.getNestingLevel() != 1) { - throw new IllegalArgumentException("methodParameter nesting level must be 1: use the nestingLevel parameter to specify the desired nestingLevel for nested type traversal"); - } - return nested(new ParameterDescriptor(methodParameter), nestingLevel); - } - - /** - * Creates a type descriptor for a nested type declared within the field. - *

For example, if the field is a {@code List} and the nesting - * level is 1, the nested type descriptor will be {@code String.class}. - *

If the field is a {@code List>} and the nesting level is - * 2, the nested type descriptor will also be a {@code String.class}. - *

If the field is a {@code Map} and the nesting level - * is 1, the nested type descriptor will be String, derived from the map value. - *

If the field is a {@code List>} and the nesting - * level is 2, the nested type descriptor will be String, derived from the map value. - *

Returns {@code null} if a nested type cannot be obtained because it was not declared. - * For example, if the field is a {@code List}, the nested type descriptor returned will be {@code null}. - * @param field the field - * @param nestingLevel the nesting level of the collection/array element or - * map key/value declaration within the field - * @return the nested type descriptor at the specified nesting level, or null - * if it could not be obtained - * @throws IllegalArgumentException if the types up to the specified nesting - * level are not of collection, array, or map types - */ - public static TypeDescriptor nested(Field field, int nestingLevel) { - return nested(new FieldDescriptor(field), nestingLevel); - } - - /** - * Creates a type descriptor for a nested type declared within the property. - *

For example, if the property is a {@code List} and the nesting - * level is 1, the nested type descriptor will be {@code String.class}. - *

If the property is a {@code List>} and the nesting level - * is 2, the nested type descriptor will also be a {@code String.class}. - *

If the property is a {@code Map} and the nesting level - * is 1, the nested type descriptor will be String, derived from the map value. - *

If the property is a {@code List>} and the nesting - * level is 2, the nested type descriptor will be String, derived from the map value. - *

Returns {@code null} if a nested type cannot be obtained because it was not declared. - * For example, if the property is a {@code List}, the nested type descriptor - * returned will be {@code null}. - * @param property the property - * @param nestingLevel the nesting level of the collection/array element or - * map key/value declaration within the property - * @return the nested type descriptor at the specified nesting level, or - * {@code null} if it could not be obtained - * @throws IllegalArgumentException if the types up to the specified nesting - * level are not of collection, array, or map types - */ - public static TypeDescriptor nested(Property property, int nestingLevel) { - return nested(new BeanPropertyDescriptor(property), nestingLevel); - } - - /** - * Create a new type descriptor for an object. - *

Use this factory method to introspect a source object before asking the conversion system to convert it to some another type. - *

If the provided object is null, returns null, else calls {@link #valueOf(Class)} to build a TypeDescriptor from the object's class. - * @param source the source object - * @return the type descriptor - */ - public static TypeDescriptor forObject(Object source) { - return (source != null ? valueOf(source.getClass()) : null); + public Class getObjectType() { + return ClassUtils.resolvePrimitiveIfNecessary(getType()); } /** @@ -279,15 +141,6 @@ public class TypeDescriptor implements Serializable { return this.type; } - /** - * Variation of {@link #getType()} that accounts for a primitive type by returning its object wrapper type. - *

This is useful for conversion service implementations that wish to normalize to object-based types - * and not work with primitive types directly. - */ - public Class getObjectType() { - return ClassUtils.resolvePrimitiveIfNecessary(getType()); - } - /** * Narrows this {@link TypeDescriptor} by setting its type to the class of the provided value. *

If the value is {@code null}, no narrowing is performed and this TypeDescriptor is returned unchanged. @@ -303,8 +156,8 @@ public class TypeDescriptor implements Serializable { if (value == null) { return this; } - return new TypeDescriptor(value.getClass(), this.elementTypeDescriptor, - this.mapKeyTypeDescriptor, this.mapValueTypeDescriptor, this.annotations); + ResolvableType narrowed = ResolvableType.forType(value.getClass(), this.resolvableType); + return new TypeDescriptor(narrowed, null, this.annotations); } /** @@ -320,8 +173,7 @@ public class TypeDescriptor implements Serializable { return null; } Assert.isAssignable(superType, getType()); - return new TypeDescriptor(superType, this.elementTypeDescriptor, - this.mapKeyTypeDescriptor, this.mapValueTypeDescriptor, this.annotations); + return new TypeDescriptor(this.resolvableType.as(superType), superType, this.annotations); } /** @@ -362,12 +214,12 @@ public class TypeDescriptor implements Serializable { */ @SuppressWarnings("unchecked") public T getAnnotation(Class annotationType) { - for (Annotation annotation : this.annotations) { + for (Annotation annotation : getAnnotations()) { if (annotation.annotationType().equals(annotationType)) { return (T) annotation; } } - for (Annotation metaAnn : this.annotations) { + for (Annotation metaAnn : getAnnotations()) { T ann = metaAnn.annotationType().getAnnotation(annotationType); if (ann != null) { return ann; @@ -406,7 +258,12 @@ public class TypeDescriptor implements Serializable { } } - // indexable type descriptor operations + private boolean isNestedAssignable(TypeDescriptor nestedTypeDescriptor, TypeDescriptor otherNestedTypeDescriptor) { + if (nestedTypeDescriptor == null || otherNestedTypeDescriptor == null) { + return true; + } + return nestedTypeDescriptor.isAssignableTo(otherNestedTypeDescriptor); + } /** * Is this type a {@link Collection} type? @@ -431,7 +288,11 @@ public class TypeDescriptor implements Serializable { */ public TypeDescriptor getElementTypeDescriptor() { assertCollectionOrArray(); - return this.elementTypeDescriptor; + if (this.resolvableType.isArray()) { + return getRelatedIfResolvable(this, this.resolvableType.getComponentType()); + } + return getRelatedIfResolvable(this, this.resolvableType.asCollection().getGeneric()); + } /** @@ -449,8 +310,6 @@ public class TypeDescriptor implements Serializable { return narrow(element, getElementTypeDescriptor()); } - // map type descriptor operations - /** * Is this type a {@link Map} type? */ @@ -466,7 +325,7 @@ public class TypeDescriptor implements Serializable { */ public TypeDescriptor getMapKeyTypeDescriptor() { assertMap(); - return this.mapKeyTypeDescriptor; + return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(0)); } /** @@ -492,7 +351,7 @@ public class TypeDescriptor implements Serializable { */ public TypeDescriptor getMapValueTypeDescriptor() { assertMap(); - return this.mapValueTypeDescriptor; + return getRelatedIfResolvable(this, this.resolvableType.asMap().getGeneric(1)); } /** @@ -509,9 +368,6 @@ public class TypeDescriptor implements Serializable { return narrow(mapValue, getMapValueTypeDescriptor()); } - - // deprecations in Spring 3.1 - /** * Returns the value of {@link TypeDescriptor#getType() getType()} for the {@link #getElementTypeDescriptor() elementTypeDescriptor}. * @deprecated in Spring 3.1 in favor of {@link #getElementTypeDescriptor()} @@ -519,7 +375,7 @@ public class TypeDescriptor implements Serializable { */ @Deprecated public Class getElementType() { - return getElementTypeDescriptor().getType(); + return getType(getElementTypeDescriptor()); } /** @@ -529,7 +385,7 @@ public class TypeDescriptor implements Serializable { */ @Deprecated public Class getMapKeyType() { - return getMapKeyTypeDescriptor().getType(); + return getType(getMapKeyTypeDescriptor()); } /** @@ -539,61 +395,13 @@ public class TypeDescriptor implements Serializable { */ @Deprecated public Class getMapValueType() { - return getMapValueTypeDescriptor().getType(); + return getType(getMapValueTypeDescriptor()); } - // package private helpers - - TypeDescriptor(AbstractDescriptor descriptor) { - this.type = descriptor.getType(); - this.elementTypeDescriptor = descriptor.getElementTypeDescriptor(); - this.mapKeyTypeDescriptor = descriptor.getMapKeyTypeDescriptor(); - this.mapValueTypeDescriptor = descriptor.getMapValueTypeDescriptor(); - this.annotations = descriptor.getAnnotations(); + private Class getType(TypeDescriptor typeDescriptor) { + return (typeDescriptor == null ? null : typeDescriptor.getType()); } - static Annotation[] nullSafeAnnotations(Annotation[] annotations) { - return annotations != null ? annotations : EMPTY_ANNOTATION_ARRAY; - } - - - // internal constructors - - private TypeDescriptor(Class type) { - this(new ClassDescriptor(type)); - } - - private TypeDescriptor(Class collectionType, TypeDescriptor elementTypeDescriptor) { - this(collectionType, elementTypeDescriptor, null, null, EMPTY_ANNOTATION_ARRAY); - } - - private TypeDescriptor(Class mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { - this(mapType, null, keyTypeDescriptor, valueTypeDescriptor, EMPTY_ANNOTATION_ARRAY); - } - - private TypeDescriptor(Class type, TypeDescriptor elementTypeDescriptor, TypeDescriptor mapKeyTypeDescriptor, - TypeDescriptor mapValueTypeDescriptor, Annotation[] annotations) { - - this.type = type; - this.elementTypeDescriptor = elementTypeDescriptor; - this.mapKeyTypeDescriptor = mapKeyTypeDescriptor; - this.mapValueTypeDescriptor = mapValueTypeDescriptor; - this.annotations = annotations; - } - - private static TypeDescriptor nested(AbstractDescriptor descriptor, int nestingLevel) { - for (int i = 0; i < nestingLevel; i++) { - descriptor = descriptor.nested(); - if (descriptor == null) { - return null; - } - } - return new TypeDescriptor(descriptor); - } - - - // internal helpers - private void assertCollectionOrArray() { if (!isCollection() && !isArray()) { throw new IllegalStateException("Not a java.util.Collection or Array"); @@ -610,23 +418,9 @@ public class TypeDescriptor implements Serializable { if (typeDescriptor != null) { return typeDescriptor.narrow(value); } - else { - return (value != null ? new TypeDescriptor(value.getClass(), null, null, null, this.annotations) : null); - } + return (value != null ? new TypeDescriptor(this.resolvableType, value.getClass(), this.annotations) : null); } - private boolean isNestedAssignable(TypeDescriptor nestedTypeDescriptor, TypeDescriptor otherNestedTypeDescriptor) { - if (nestedTypeDescriptor == null || otherNestedTypeDescriptor == null) { - return true; - } - return nestedTypeDescriptor.isAssignableTo(otherNestedTypeDescriptor); - } - - private String wildcard(TypeDescriptor typeDescriptor) { - return typeDescriptor != null ? typeDescriptor.toString() : "?"; - } - - @Override public boolean equals(Object obj) { if (this == obj) { @@ -639,20 +433,20 @@ public class TypeDescriptor implements Serializable { if (!ObjectUtils.nullSafeEquals(this.type, other.type)) { return false; } - if (this.annotations.length != other.annotations.length) { + if (getAnnotations().length != other.getAnnotations().length) { return false; } - for (Annotation ann : this.annotations) { + for (Annotation ann : this.getAnnotations()) { if (other.getAnnotation(ann.annotationType()) == null) { return false; } } if (isCollection() || isArray()) { - return ObjectUtils.nullSafeEquals(this.elementTypeDescriptor, other.elementTypeDescriptor); + return ObjectUtils.nullSafeEquals(this.getElementTypeDescriptor(), other.getElementTypeDescriptor()); } else if (isMap()) { - return ObjectUtils.nullSafeEquals(this.mapKeyTypeDescriptor, other.mapKeyTypeDescriptor) && - ObjectUtils.nullSafeEquals(this.mapValueTypeDescriptor, other.mapValueTypeDescriptor); + return ObjectUtils.nullSafeEquals(this.getMapKeyTypeDescriptor(), other.getMapKeyTypeDescriptor()) && + ObjectUtils.nullSafeEquals(this.getMapValueTypeDescriptor(), other.getMapValueTypeDescriptor()); } else { return true; @@ -667,18 +461,196 @@ public class TypeDescriptor implements Serializable { @Override public String toString() { StringBuilder builder = new StringBuilder(); - for (Annotation ann : this.annotations) { + for (Annotation ann : getAnnotations()) { builder.append("@").append(ann.annotationType().getName()).append(' '); } - builder.append(ClassUtils.getQualifiedName(getType())); - if (isMap()) { - builder.append("<").append(wildcard(this.mapKeyTypeDescriptor)); - builder.append(", ").append(wildcard(this.mapValueTypeDescriptor)).append(">"); - } - else if (isCollection()) { - builder.append("<").append(wildcard(this.elementTypeDescriptor)).append(">"); - } + builder.append(this.resolvableType.toString()); return builder.toString(); } + /** + * Create a new type descriptor from the given type. + *

Use this to instruct the conversion system to convert an object to a + * specific target type, when no type location such as a method parameter or + * field is available to provide additional conversion context. + *

Generally prefer use of {@link #forObject(Object)} for constructing type + * descriptors from source objects, as it handles the {@code null} object case. + * @param type the class + * @return the type descriptor + */ + public static TypeDescriptor valueOf(Class type) { + Assert.notNull(type, "Type must not be null"); + TypeDescriptor desc = commonTypesCache.get(type); + return (desc != null ? desc : new TypeDescriptor(ResolvableType.forClass(type), null, null)); + } + + /** + * Create a new type descriptor from a {@link java.util.Collection} type. + *

Useful for converting to typed Collections. + *

For example, a {@code List} could be converted to a + * {@code List} by converting to a targetType built with this method. + * The method call to construct such a {@code TypeDescriptor} would look something + * like: {@code collection(List.class, TypeDescriptor.valueOf(EmailAddress.class));} + * @param collectionType the collection type, which must implement {@link Collection}. + * @param elementTypeDescriptor a descriptor for the collection's element type, + * used to convert collection elements + * @return the collection type descriptor + */ + public static TypeDescriptor collection(Class collectionType, TypeDescriptor elementTypeDescriptor) { + Assert.notNull(collectionType, "CollectionType must not be null"); + Assert.notNull(elementTypeDescriptor, "ElementTypeDesciptor must not be null"); + if (!Collection.class.isAssignableFrom(collectionType)) { + throw new IllegalArgumentException("collectionType must be a java.util.Collection"); + } + return new TypeDescriptor(ResolvableType.forClassWithGenerics(collectionType, + elementTypeDescriptor.resolvableType), null, null); + } + + /** + * Create a new type descriptor from a {@link java.util.Map} type. + *

Useful for converting to typed Maps. + *

For example, a Map<String, String> could be converted to a Map<Id, EmailAddress> by converting to a targetType built with this method: + * The method call to construct such a TypeDescriptor would look something like: map(Map.class, TypeDescriptor.valueOf(Id.class), TypeDescriptor.valueOf(EmailAddress.class)); + * @param mapType the map type, which must implement {@link Map} + * @param keyTypeDescriptor a descriptor for the map's key type, used to convert map keys + * @param valueTypeDescriptor the map's value type, used to convert map values + * @return the map type descriptor + */ + public static TypeDescriptor map(Class mapType, TypeDescriptor keyTypeDescriptor, TypeDescriptor valueTypeDescriptor) { + if (!Map.class.isAssignableFrom(mapType)) { + throw new IllegalArgumentException("mapType must be a java.util.Map"); + } + return new TypeDescriptor(ResolvableType.forClassWithGenerics(mapType, + keyTypeDescriptor.resolvableType, valueTypeDescriptor.resolvableType), null, null); + } + + /** + * Create a new type descriptor as an array of the specified type. + *

For example to create a {@code Map[]} use + * {@code TypeDescriptor.array(TypeDescriptor.map(Map.class, TypeDescriptor.value(String.class), TypeDescriptor.value(String.class)))}. + * @param elementTypeDescriptor the {@link TypeDescriptor} of the array element or {@code null} + * @return an array {@link TypeDescriptor} or {@code null} if {@code elementTypeDescriptor} is {@code null} + * @since 3.2.1 + */ + public static TypeDescriptor array(TypeDescriptor elementTypeDescriptor) { + if(elementTypeDescriptor == null) { + return null; + } + return new TypeDescriptor( + ResolvableType.forArrayComponent(elementTypeDescriptor.resolvableType), + null, elementTypeDescriptor.getAnnotations()); + } + + /** + * Creates a type descriptor for a nested type declared within the method parameter. + *

For example, if the methodParameter is a {@code List} and the + * nesting level is 1, the nested type descriptor will be String.class. + *

If the methodParameter is a {@code List>} and the nesting + * level is 2, the nested type descriptor will also be a String.class. + *

If the methodParameter is a {@code Map} and the nesting + * level is 1, the nested type descriptor will be String, derived from the map value. + *

If the methodParameter is a {@code List>} and the + * nesting level is 2, the nested type descriptor will be String, derived from the map value. + *

Returns {@code null} if a nested type cannot be obtained because it was not declared. + * For example, if the method parameter is a {@code List}, the nested type + * descriptor returned will be {@code null}. + * @param methodParameter the method parameter with a nestingLevel of 1 + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the method parameter + * @return the nested type descriptor at the specified nesting level, or null + * if it could not be obtained + * @throws IllegalArgumentException if the nesting level of the input + * {@link MethodParameter} argument is not 1 + * @throws IllegalArgumentException if the types up to the specified nesting + * level are not of collection, array, or map types + */ + public static TypeDescriptor nested(MethodParameter methodParameter, int nestingLevel) { + if (methodParameter.getNestingLevel() != 1) { + throw new IllegalArgumentException("methodParameter nesting level must be 1: use the nestingLevel parameter to specify the desired nestingLevel for nested type traversal"); + } + return nested(new TypeDescriptor(methodParameter), nestingLevel); + } + + /** + * Creates a type descriptor for a nested type declared within the field. + *

For example, if the field is a {@code List} and the nesting + * level is 1, the nested type descriptor will be {@code String.class}. + *

If the field is a {@code List>} and the nesting level is + * 2, the nested type descriptor will also be a {@code String.class}. + *

If the field is a {@code Map} and the nesting level + * is 1, the nested type descriptor will be String, derived from the map value. + *

If the field is a {@code List>} and the nesting + * level is 2, the nested type descriptor will be String, derived from the map value. + *

Returns {@code null} if a nested type cannot be obtained because it was not declared. + * For example, if the field is a {@code List}, the nested type descriptor returned will be {@code null}. + * @param field the field + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the field + * @return the nested type descriptor at the specified nesting level, or null + * if it could not be obtained + * @throws IllegalArgumentException if the types up to the specified nesting + * level are not of collection, array, or map types + */ + public static TypeDescriptor nested(Field field, int nestingLevel) { + return nested(new TypeDescriptor(field), nestingLevel); + } + + /** + * Creates a type descriptor for a nested type declared within the property. + *

For example, if the property is a {@code List} and the nesting + * level is 1, the nested type descriptor will be {@code String.class}. + *

If the property is a {@code List>} and the nesting level + * is 2, the nested type descriptor will also be a {@code String.class}. + *

If the property is a {@code Map} and the nesting level + * is 1, the nested type descriptor will be String, derived from the map value. + *

If the property is a {@code List>} and the nesting + * level is 2, the nested type descriptor will be String, derived from the map value. + *

Returns {@code null} if a nested type cannot be obtained because it was not declared. + * For example, if the property is a {@code List}, the nested type descriptor + * returned will be {@code null}. + * @param property the property + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the property + * @return the nested type descriptor at the specified nesting level, or + * {@code null} if it could not be obtained + * @throws IllegalArgumentException if the types up to the specified nesting + * level are not of collection, array, or map types + */ + public static TypeDescriptor nested(Property property, int nestingLevel) { + return nested(new TypeDescriptor(property), nestingLevel); + } + + /** + * Create a new type descriptor for an object. + *

Use this factory method to introspect a source object before asking the conversion system to convert it to some another type. + *

If the provided object is null, returns null, else calls {@link #valueOf(Class)} to build a TypeDescriptor from the object's class. + * @param source the source object + * @return the type descriptor + */ + public static TypeDescriptor forObject(Object source) { + return (source != null ? valueOf(source.getClass()) : null); + } + + private static TypeDescriptor nested(TypeDescriptor typeDescriptor, int nestingLevel) { + ResolvableType nested = typeDescriptor.resolvableType; + for (int i = 0; i < nestingLevel; i++) { + if (Object.class.equals(nested.getType())) { + // could be a collection type but we don't know about its element type, + // so let's just assume there is an element type of type Object + } else { + nested = nested.getNested(2); + } + } + Assert.state(nested != ResolvableType.NONE, "Unable to obtain nested generic from " + + typeDescriptor + " at level " + nestingLevel); + return getRelatedIfResolvable(typeDescriptor, nested); + } + + private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, ResolvableType type) { + if (type.resolve() == null) { + return null; + } + return new TypeDescriptor(type, null, source.annotations); + } + } diff --git a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java index 7af9d19f87..22107b085d 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java @@ -32,14 +32,20 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import org.junit.Ignore; import org.junit.Test; import org.springframework.core.MethodParameter; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; /** + * Tests for {@link TypeDescriptor}. + * * @author Keith Donald * @author Andy Clement * @author Phillip Webb @@ -105,7 +111,7 @@ public class TypeDescriptorTests { assertEquals(List.class, desc.getType()); assertEquals(List.class, desc.getObjectType()); assertEquals("java.util.List", desc.getName()); - assertEquals("java.util.List>>", desc.toString()); + assertEquals("java.util.List>>>", desc.toString()); assertTrue(!desc.isPrimitive()); assertEquals(0, desc.getAnnotations().length); assertTrue(desc.isCollection()); @@ -435,7 +441,7 @@ public class TypeDescriptorTests { assertTrue(typeDescriptor.isArray()); assertEquals(List.class,typeDescriptor.getElementTypeDescriptor().getType()); assertEquals(String.class, typeDescriptor.getElementTypeDescriptor().getElementTypeDescriptor().getType()); - assertEquals("java.util.List[]",typeDescriptor.toString()); + assertEquals("java.util.List[]",typeDescriptor.toString()); } @Test @@ -812,6 +818,31 @@ public class TypeDescriptorTests { public Map isAssignableMapKeyValueTypes; + @Test + public void multiValueMap() throws Exception { + TypeDescriptor td = new TypeDescriptor(getClass().getField("multiValueMap")); + assertTrue(td.isMap()); + assertEquals(String.class, td.getMapKeyTypeDescriptor().getType()); + assertEquals(List.class, td.getMapValueTypeDescriptor().getType()); + assertEquals(Integer.class, + td.getMapValueTypeDescriptor().getElementTypeDescriptor().getType()); + } + + public MultiValueMap multiValueMap = new LinkedMultiValueMap(); + + @Test + public void passDownGeneric() throws Exception { + TypeDescriptor td = new TypeDescriptor(getClass().getField("passDownGeneric")); + assertEquals(List.class, td.getElementTypeDescriptor().getType()); + assertEquals(Set.class, td.getElementTypeDescriptor().getElementTypeDescriptor().getType()); + assertEquals(Integer.class, td.getElementTypeDescriptor().getElementTypeDescriptor().getElementTypeDescriptor().getType()); + } + + public PassDownGeneric passDownGeneric = new PassDownGeneric(); + + public static class PassDownGeneric extends ArrayList>> { + } + @Test public void testUpCast() throws Exception { Property property = new Property(getClass(), getClass().getMethod("getProperty"), diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java index 9f3ca159cc..9f4dab0ee9 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java @@ -25,11 +25,13 @@ import java.util.Map; import org.junit.Before; 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.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; public class MapToMapConverterTests { @@ -217,6 +219,36 @@ public class MapToMapConverterTests { assertEquals(NoDefaultConstructorMap.class, result.getClass()); } + public MultiValueMap multiValueMapTarget; + + @Test + @SuppressWarnings("unchecked") + public void multiValueMapToMultiValueMap() throws Exception { + DefaultConversionService.addDefaultConverters(conversionService); + MultiValueMap source = new LinkedMultiValueMap(); + source.put("a", Arrays.asList(1, 2, 3)); + source.put("b", Arrays.asList(4, 5, 6)); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("multiValueMapTarget")); + MultiValueMap converted = (MultiValueMap) conversionService.convert(source, targetType); + assertThat(converted.size(), equalTo(2)); + assertThat(converted.get("a"), equalTo(Arrays.asList("1", "2", "3"))); + assertThat(converted.get("b"), equalTo(Arrays.asList("4", "5", "6"))); + } + + @Test + @SuppressWarnings("unchecked") + public void mapToMultiValueMap() throws Exception { + DefaultConversionService.addDefaultConverters(conversionService); + Map source = new HashMap(); + source.put("a", 1); + source.put("b", 2); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("multiValueMapTarget")); + MultiValueMap converted = (MultiValueMap) conversionService.convert(source, targetType); + assertThat(converted.size(), equalTo(2)); + assertThat(converted.get("a"), equalTo(Arrays.asList("1"))); + assertThat(converted.get("b"), equalTo(Arrays.asList("2"))); + } + @SuppressWarnings("serial") public static class NoDefaultConstructorMap extends HashMap { public NoDefaultConstructorMap(Map m) {