map-to-map tests

This commit is contained in:
Keith Donald 2009-04-10 15:07:23 +00:00
parent 5649f2f31d
commit 76f63f8b64
3 changed files with 243 additions and 50 deletions

View File

@ -36,10 +36,10 @@ import org.springframework.util.Assert;
*/
public class TypeDescriptor {
/**
* constant value typeDescriptor for the type of a null value
*/
public final static TypeDescriptor NULL_TYPE_DESCRIPTOR = new TypeDescriptor((Class<?>)null);
/**
* constant value typeDescriptor for the type of a null value
*/
public final static TypeDescriptor NULL_TYPE_DESCRIPTOR = new TypeDescriptor((Class<?>) null);
private MethodParameter methodParameter;
@ -48,19 +48,19 @@ public class TypeDescriptor {
private Annotation[] cachedFieldAnnotations;
private Class<?> type;
/**
* Creates a new descriptor for the given type.
* Use this constructor when a bound value comes from a source such as a Map or collection, where no additional binding metadata is available.
* Creates a new descriptor for the given type. Use this constructor when a bound value comes from a source such as
* a Map or collection, where no additional binding metadata is available.
* @param type the actual type
*/
public TypeDescriptor(Class<?> type) {
this.type = type;
}
/**
* Create a new descriptor for a method or constructor parameter.
* Use this constructor when a bound value originates from a method parameter, such as a setter method argument.
* Create a new descriptor for a method or constructor parameter. Use this constructor when a bound value originates
* from a method parameter, such as a setter method argument.
* @param methodParameter the MethodParameter to wrap
*/
public TypeDescriptor(MethodParameter methodParameter) {
@ -69,8 +69,7 @@ public class TypeDescriptor {
}
/**
* Create a new descriptor for a field.
* Use this constructor when a bound value originates from a field.
* Create a new descriptor for a field. Use this constructor when a bound value originates from a field.
* @param field the field to wrap
*/
public TypeDescriptor(Field field) {
@ -88,7 +87,7 @@ public class TypeDescriptor {
return type;
} else if (field != null) {
return field.getType();
} else if (methodParameter!=null) {
} else if (methodParameter != null) {
return methodParameter.getParameterType();
} else {
return null;
@ -101,6 +100,9 @@ public class TypeDescriptor {
*/
public Class<?> getWrapperTypeIfPrimitive() {
Class<?> type = getType();
if (type == null) {
return null;
}
if (type.isPrimitive()) {
if (type.equals(int.class)) {
return Integer.class;
@ -125,12 +127,17 @@ public class TypeDescriptor {
return type;
}
}
/**
* Returns the name of this type; the fully qualified classname.
*/
public String getName() {
return getType().getName();
Class<?> type = getType();
if (type != null) {
return getType().getName();
} else {
return null;
}
}
/**
@ -138,26 +145,23 @@ public class TypeDescriptor {
*/
public boolean isArray() {
Class<?> type = getType();
return (type==null?false:type.isArray());
if (type != null) {
return type.isArray();
} else {
return false;
}
}
/**
* Is this type a {@link Collection} type?
*/
public boolean isCollection() {
return Collection.class.isAssignableFrom(getType());
return isTypeAssignableTo(Collection.class);
}
/**
* Is this type a {@link Map} type?
*/
public boolean isMap() {
return Map.class.isAssignableFrom(getType());
}
/**
* If this type is an array type or {@link Collection} type, returns the underlying element type.
* Returns null if the type is neither an array or collection.
* If this type is an array type or {@link Collection} type, returns the underlying element type. Returns null if
* the type is neither an array or collection.
*/
public Class<?> getElementType() {
if (isArray()) {
@ -168,15 +172,34 @@ public class TypeDescriptor {
return null;
}
}
/**
* Is this type a {@link Map} type?
*/
public boolean isMap() {
return isTypeAssignableTo(Map.class);
}
/**
* Is this descriptor for a map where the key type and value type are known?
*/
public boolean isMapEntryTypeKnown() {
return isMap() && getMapKeyType() != null && getMapValueType() != null;
}
/**
* Determine the generic key type of the wrapped Map parameter/field, if any.
*
* @return the generic type, or <code>null</code> if none
*/
public Class<?> getMapKeyType() {
return (field != null ? GenericCollectionTypeResolver.getMapKeyFieldType(field) : GenericCollectionTypeResolver
.getMapKeyParameterType(methodParameter));
if (field != null) {
return GenericCollectionTypeResolver.getMapKeyFieldType(field);
} else if (methodParameter != null) {
return GenericCollectionTypeResolver.getMapKeyParameterType(methodParameter);
} else {
return null;
}
}
/**
@ -185,8 +208,13 @@ public class TypeDescriptor {
* @return the generic type, or <code>null</code> if none
*/
public Class<?> getMapValueType() {
return (field != null ? GenericCollectionTypeResolver.getMapValueFieldType(field)
: GenericCollectionTypeResolver.getMapValueParameterType(methodParameter));
if (field != null) {
return GenericCollectionTypeResolver.getMapValueFieldType(field);
} else if (methodParameter != null) {
return GenericCollectionTypeResolver.getMapValueParameterType(methodParameter);
} else {
return null;
}
}
/**
@ -198,8 +226,10 @@ public class TypeDescriptor {
cachedFieldAnnotations = field.getAnnotations();
}
return cachedFieldAnnotations;
} else {
} else if (methodParameter != null) {
return methodParameter.getParameterAnnotations();
} else {
return null;
}
}
@ -229,14 +259,24 @@ public class TypeDescriptor {
* Returns true if this type is an abstract class.
*/
public boolean isAbstractClass() {
return !getType().isInterface() && Modifier.isAbstract(getType().getModifiers());
Class<?> type = getType();
if (type != null) {
return !getType().isInterface() && Modifier.isAbstract(getType().getModifiers());
} else {
return false;
}
}
/**
* Is the obj an instance of this type?
*/
public boolean isInstance(Object obj) {
return getType().isInstance(obj);
Class<?> type = getType();
if (type != null) {
return getType().isInstance(obj);
} else {
return false;
}
}
/**
@ -247,7 +287,6 @@ public class TypeDescriptor {
public boolean isAssignableTo(TypeDescriptor targetType) {
return targetType.getType().isAssignableFrom(getType());
}
/**
* Creates a new type descriptor for the given class.
@ -265,13 +304,13 @@ public class TypeDescriptor {
* @return the type descriptor
*/
public static TypeDescriptor forObject(Object object) {
if (object==null) {
if (object == null) {
return NULL_TYPE_DESCRIPTOR;
} else {
return valueOf(object.getClass());
}
}
/**
* @return a textual representation of the type descriptor (eg. Map<String,Foo>) for use in messages
*/
@ -288,13 +327,13 @@ public class TypeDescriptor {
stringValue.append(clazz.getName());
if (isCollection()) {
Class<?> collectionType = getCollectionElementType();
if (collectionType!=null) {
if (collectionType != null) {
stringValue.append("<").append(collectionType.getName()).append(">");
}
} else if (isMap()) {
Class<?> keyType = getMapKeyType();
Class<?> valType = getMapValueType();
if (keyType!=null && valType!=null) {
if (keyType != null && valType != null) {
stringValue.append("<").append(keyType.getName()).append(",");
stringValue.append(valType).append(">");
}
@ -303,20 +342,29 @@ public class TypeDescriptor {
return stringValue.toString();
}
// internal helpers
private Class<?> getArrayComponentType() {
return getType().getComponentType();
}
@SuppressWarnings("unchecked")
private Class<?> getCollectionElementType() {
if (type != null) {
return GenericCollectionTypeResolver.getCollectionType((Class<? extends Collection>) type);
} else if (field != null) {
return GenericCollectionTypeResolver.getCollectionFieldType(field);
} else {
return GenericCollectionTypeResolver.getCollectionParameterType(methodParameter);
return GenericCollectionTypeResolver.getCollectionParameterType(methodParameter);
}
}
private boolean isTypeAssignableTo(Class<?> clazz) {
Class<?> type = getType();
if (type != null) {
return clazz.isAssignableFrom(type);
} else {
return false;
}
}

View File

@ -23,36 +23,58 @@ import java.util.TreeMap;
import org.springframework.core.convert.ConversionExecutionException;
import org.springframework.core.convert.ConversionExecutor;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
/**
* Converts from one map to another map, with support for converting individual map elements based on generic type information.
* @author Keith Donald
*/
class MapToMap implements ConversionExecutor {
private TypeDescriptor sourceType;
private TypeDescriptor targetType;
private GenericConversionService conversionService;
private ConversionService conversionService;
public MapToMap(TypeDescriptor sourceType, TypeDescriptor targetType, GenericConversionService conversionService) {
private EntryConverter entryConverter;
/**
* Creates a new map-to-map converter
* @param sourceType the source map type
* @param targetType the target map type
* @param conversionService the conversion service
*/
public MapToMap(TypeDescriptor sourceType, TypeDescriptor targetType, ConversionService conversionService) {
this.sourceType = sourceType;
this.targetType = targetType;
this.conversionService = conversionService;
this.entryConverter = createEntryConverter();
}
private EntryConverter createEntryConverter() {
if (sourceType.isMapEntryTypeKnown() && targetType.isMapEntryTypeKnown()) {
ConversionExecutor keyConverter = conversionService.getConversionExecutor(sourceType.getMapKeyType(),
TypeDescriptor.valueOf(targetType.getMapKeyType()));
ConversionExecutor valueConverter = conversionService.getConversionExecutor(sourceType.getMapValueType(),
TypeDescriptor.valueOf(targetType.getMapValueType()));
return new EntryConverter(keyConverter, valueConverter);
} else {
return EntryConverter.NO_OP_INSTANCE;
}
}
@SuppressWarnings("unchecked")
public Object execute(Object source) throws ConversionExecutionException {
try {
// TODO shouldn't do all this if generic info is null - should cache executor after first iteration?
Map map = (Map) source;
Map targetMap = (Map) getImpl(targetType.getType()).newInstance();
EntryConverter converter = getEntryConverter(map);
Iterator<Map.Entry<?, ?>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
Object key = entry.getKey();
Object value = entry.getValue();
key = conversionService.executeConversion(key, TypeDescriptor.valueOf(targetType.getMapKeyType()));
value = conversionService.executeConversion(value, TypeDescriptor.valueOf(targetType.getMapValueType()));
targetMap.put(key, value);
targetMap.put(converter.convertKey(entry.getKey()), converter.convertValue(entry.getValue()));
}
return targetMap;
} catch (Exception e) {
@ -60,6 +82,37 @@ class MapToMap implements ConversionExecutor {
}
}
private EntryConverter getEntryConverter(Map<?, ?> map) {
EntryConverter entryConverter = this.entryConverter;
if (entryConverter == EntryConverter.NO_OP_INSTANCE) {
Class<?> targetKeyType = targetType.getMapKeyType();
Class<?> targetValueType = targetType.getMapValueType();
if (targetKeyType != null && targetValueType != null) {
ConversionExecutor keyConverter = null;
ConversionExecutor valueConverter = null;
Iterator<?> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) it.next();
Object key = entry.getKey();
Object value = entry.getValue();
if (keyConverter == null && key != null) {
keyConverter = conversionService.getConversionExecutor(key.getClass(), TypeDescriptor
.valueOf(targetKeyType));
}
if (valueConverter == null && value != null) {
valueConverter = conversionService.getConversionExecutor(value.getClass(), TypeDescriptor
.valueOf(targetValueType));
}
if (keyConverter != null && valueConverter != null) {
break;
}
}
entryConverter = new EntryConverter(keyConverter, valueConverter);
}
}
return entryConverter;
}
static Class<?> getImpl(Class<?> targetClass) {
if (targetClass.isInterface()) {
if (Map.class.equals(targetClass)) {
@ -74,4 +127,39 @@ class MapToMap implements ConversionExecutor {
}
}
private static class EntryConverter {
public static final EntryConverter NO_OP_INSTANCE = new EntryConverter();
private ConversionExecutor keyConverter;
private ConversionExecutor valueConverter;
private EntryConverter() {
}
public EntryConverter(ConversionExecutor keyConverter, ConversionExecutor valueConverter) {
this.keyConverter = keyConverter;
this.valueConverter = valueConverter;
}
public Object convertKey(Object key) {
if (keyConverter != null) {
return keyConverter.execute(key);
} else {
return key;
}
}
public Object convertValue(Object value) {
if (valueConverter != null) {
return valueConverter.execute(value);
} else {
return value;
}
}
}
}

View File

@ -0,0 +1,57 @@
package org.springframework.core.convert.service;
import static org.junit.Assert.assertEquals;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.core.convert.TypeDescriptor;
public class MapToMapTests {
@Test
public void testMapToMapConversion() throws Exception {
DefaultConversionService service = new DefaultConversionService();
MapToMap c = new MapToMap(new TypeDescriptor(getClass().getField("source")),
new TypeDescriptor(getClass().getField("bindTarget")), service);
source.put("1", "BAR");
source.put("2", "BAZ");
Map result = (Map) c.execute(source);
assertEquals(FooEnum.BAR, result.get(1));
assertEquals(FooEnum.BAZ, result.get(2));
}
@Test
public void testMapToMapConversionNoGenericInfoOnSource() throws Exception {
DefaultConversionService service = new DefaultConversionService();
MapToMap c = new MapToMap(TypeDescriptor.valueOf(Map.class),
new TypeDescriptor(getClass().getField("bindTarget")), service);
source.put("1", "BAR");
source.put("2", "BAZ");
Map result = (Map) c.execute(source);
assertEquals(FooEnum.BAR, result.get(1));
assertEquals(FooEnum.BAZ, result.get(2));
}
@Test
public void testMapToMapConversionNoGenericInfo() throws Exception {
DefaultConversionService service = new DefaultConversionService();
MapToMap c = new MapToMap(TypeDescriptor.valueOf(Map.class),
TypeDescriptor.valueOf(Map.class), service);
source.put("1", "BAR");
source.put("2", "BAZ");
Map result = (Map) c.execute(source);
assertEquals("BAR", result.get("1"));
assertEquals("BAZ", result.get("2"));
}
public Map<String, String> source = new HashMap<String, String>();
public Map<Integer, FooEnum> bindTarget = new HashMap<Integer, FooEnum>();
public static enum FooEnum {
BAR, BAZ;
}
}