From 76f63f8b64b0ac64d95fae7d30903f1fd580abc4 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Fri, 10 Apr 2009 15:07:23 +0000 Subject: [PATCH] map-to-map tests --- .../core/convert/TypeDescriptor.java | 132 ++++++++++++------ .../core/convert/service/MapToMap.java | 104 ++++++++++++-- .../core/convert/service/MapToMapTests.java | 57 ++++++++ 3 files changed, 243 insertions(+), 50 deletions(-) create mode 100644 org.springframework.core/src/test/java/org/springframework/core/convert/service/MapToMapTests.java diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index a0dcc15516..7bd9387e4c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -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 null 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 null 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) 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) 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; } } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/service/MapToMap.java b/org.springframework.core/src/main/java/org/springframework/core/convert/service/MapToMap.java index bf8831a305..90db0082c8 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/service/MapToMap.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/service/MapToMap.java @@ -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> 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; + } + } + + } + } diff --git a/org.springframework.core/src/test/java/org/springframework/core/convert/service/MapToMapTests.java b/org.springframework.core/src/test/java/org/springframework/core/convert/service/MapToMapTests.java new file mode 100644 index 0000000000..586be04fa1 --- /dev/null +++ b/org.springframework.core/src/test/java/org/springframework/core/convert/service/MapToMapTests.java @@ -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 source = new HashMap(); + public Map bindTarget = new HashMap(); + + public static enum FooEnum { + BAR, BAZ; + } + +}