From 518f98d4c35d21243747fc159ccc94a3265b42c5 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Tue, 31 Mar 2009 15:06:22 +0000 Subject: [PATCH] polish --- .../core/convert/TypeDescriptor.java | 111 +++++++++++++----- .../core/convert/TypedValue.java | 33 +++++- .../service/GenericConversionService.java | 29 +++-- .../core/convert/service/NoOpConverter.java | 6 + .../GenericConversionServiceTests.java | 88 +++++++++----- 5 files changed, 194 insertions(+), 73 deletions(-) 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 9620b75e03..a8cedc6b27 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 @@ -10,7 +10,7 @@ import org.springframework.core.MethodParameter; import org.springframework.util.Assert; /** - * Metadata about a retrieved value or value type. + * Type metadata about a bindable value. * * @author Keith Donald */ @@ -24,13 +24,18 @@ public class TypeDescriptor { 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. + * @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. * @param methodParameter the MethodParameter to wrap */ public TypeDescriptor(MethodParameter methodParameter) { @@ -39,8 +44,8 @@ public class TypeDescriptor { } /** - * Create a new descriptor for a field. Considers the dependency as 'eager'. - * + * 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) { @@ -63,6 +68,10 @@ public class TypeDescriptor { } } + /** + * If the actual type is a primitive, returns its wrapper type, else just returns {@link #getType()}. + * @return the wrapper type if the underlying type is a primitive, else the actual type as-is + */ public Class getWrapperTypeIfPrimitive() { Class type = getType(); if (type.isPrimitive()) { @@ -90,42 +99,41 @@ public class TypeDescriptor { } } + /** + * Returns the name of this type; the fully qualified classname. + */ public String getName() { return getType().getName(); } - + + /** + * Is this type an array type? + */ public boolean isArray() { return getType().isArray(); } - - public Class getElementType() { - return isArray() ? getArrayComponentType() : getCollectionElementType(); - } - - public Class getArrayComponentType() { - return getType().getComponentType(); - } - public boolean isInstance(Object source) { - return getType().isInstance(source); + /** + * Is this type a {@link Collection} type? + */ + public boolean isCollection() { + return Collection.class.isAssignableFrom(getType()); } - /** - * Determine the generic element type of the wrapped Collection parameter/field, if any. - * - * @return the generic type, or null if none + * 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 getCollectionElementType() { - if (type != null) { - return GenericCollectionTypeResolver.getCollectionType((Class) type); - } else if (field != null) { - return GenericCollectionTypeResolver.getCollectionFieldType(field); + public Class getElementType() { + if (isArray()) { + return getArrayComponentType(); + } else if (isCollection()) { + return getCollectionElementType(); } else { - return GenericCollectionTypeResolver.getCollectionParameterType(methodParameter); + return null; } } - + /** * Determine the generic key type of the wrapped Map parameter/field, if any. * @@ -182,12 +190,53 @@ public class TypeDescriptor { return field; } - public boolean isCollection() { - return Collection.class.isAssignableFrom(getType()); - } - + /** + * Returns true if this type is an abstract class. + */ public boolean isAbstractClass() { return !getType().isInterface() && Modifier.isAbstract(getType().getModifiers()); } -} + /** + * Is the obj an instance of this type? + */ + public boolean isInstance(Object obj) { + return getType().isInstance(obj); + } + + /** + * Returns true if an object this type can be assigned to a rereference of given targetType. + * @param targetType the target type + * @return true if this type is assignable to the target + */ + public boolean isAssignableTo(TypeDescriptor targetType) { + return targetType.getType().isAssignableFrom(getType()); + } + + + /** + * Creates a new type descriptor for the given class. + * @param type the class + * @return the type descriptor + */ + public static TypeDescriptor valueOf(Class type) { + return new TypeDescriptor(type); + } + + // internal helpers + + private Class getArrayComponentType() { + return getType().getComponentType(); + } + + private Class getCollectionElementType() { + if (type != null) { + return GenericCollectionTypeResolver.getCollectionType((Class) type); + } else if (field != null) { + return GenericCollectionTypeResolver.getCollectionFieldType(field); + } else { + return GenericCollectionTypeResolver.getCollectionParameterType(methodParameter); + } + } + +} \ No newline at end of file diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/TypedValue.java b/org.springframework.core/src/main/java/org/springframework/core/convert/TypedValue.java index ebdbe93467..4a0ace1ba2 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/TypedValue.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/TypedValue.java @@ -1,18 +1,40 @@ package org.springframework.core.convert; /** - * A value retrieved from some location such as a field access or getter invocation. + * A value with additional context about its type. + * The additional context provides information about how the value was obtained; for example from a field read or getter return value. + * This additional context allows access to generic information associated with a field, return value, or method argument. + * It also allows access to field-level or method-level annotations. + * All of this context can be utilized when performing a type conversion as part of a data binding routine. * * @author Keith Donald */ public class TypedValue { + /** + * The NULL TypedValue object. + */ + public static final TypedValue NULL = new TypedValue(null); + private final Object value; private final TypeDescriptor typeDescriptor; /** - * Creates a typed value. + * Creates a new typed value. + * @param value the actual value (may be null) + */ + public TypedValue(Object value) { + this.value = value; + if (this.value != null) { + typeDescriptor = TypeDescriptor.valueOf(value.getClass()); + } else { + typeDescriptor = null; + } + } + + /** + * Creates a new typed value. * @param value the actual value (may be null) * @param typeDescriptor the value type descriptor (may be null) */ @@ -35,4 +57,11 @@ public class TypedValue { return typeDescriptor; } + /** + * True if both the actual value and type descriptor are null. + */ + public boolean isNull() { + return value == null && typeDescriptor == null; + } + } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/service/GenericConversionService.java b/org.springframework.core/src/main/java/org/springframework/core/convert/service/GenericConversionService.java index 50935ff868..696b53add4 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/service/GenericConversionService.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/service/GenericConversionService.java @@ -167,11 +167,19 @@ public class GenericConversionService implements ConversionService { public Object executeConversion(TypedValue source, TypeDescriptor targetType) throws ConversionExecutorNotFoundException, ConversionException { + Assert.notNull(source, "The source to convert from is required"); + if (source.isNull()) { + return null; + } return getConversionExecutor(source.getTypeDescriptor(), targetType).execute(source.getValue()); } public Object executeConversion(String converterId, TypedValue source, TypeDescriptor targetType) throws ConversionExecutorNotFoundException, ConversionException { + Assert.notNull(source, "The source to convert from is required"); + if (source.isNull()) { + return null; + } return getConversionExecutor(converterId, source.getTypeDescriptor(), targetType).execute(source.getValue()); } @@ -206,21 +214,20 @@ public class GenericConversionService implements ConversionService { throw new UnsupportedOperationException("Object to collection not yet supported"); } } - Class sourceClass = sourceType.getWrapperTypeIfPrimitive(); - Class targetClass = targetType.getWrapperTypeIfPrimitive(); - Converter converter = findRegisteredConverter(sourceClass, targetClass); + Converter converter = findRegisteredConverter(sourceType, targetType); if (converter != null) { - // we found a converter return new StaticConversionExecutor(sourceType, targetType, converter); } else { - SuperConverter superConverter = findRegisteredSuperConverter(sourceClass, targetClass); + SuperConverter superConverter = findRegisteredSuperConverter(sourceType, targetType); if (superConverter != null) { return new StaticSuperConversionExecutor(sourceType, targetType, superConverter); } if (parent != null) { - // try the parent return parent.getConversionExecutor(sourceType, targetType); } else { + if (sourceType.isAssignableTo(targetType)) { + return new StaticConversionExecutor(sourceType, targetType, NoOpConverter.INSTANCE); + } throw new ConversionExecutorNotFoundException(sourceType, targetType, "No ConversionExecutor found for converting from sourceType [" + sourceType.getName() + "] to targetType [" + targetType.getName() + "]"); @@ -306,7 +313,9 @@ public class GenericConversionService implements ConversionService { return sourceMap; } - private Converter findRegisteredConverter(Class sourceClass, Class targetClass) { + private Converter findRegisteredConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + Class sourceClass = sourceType.getWrapperTypeIfPrimitive(); + Class targetClass = targetType.getWrapperTypeIfPrimitive(); if (sourceClass.isInterface()) { LinkedList classQueue = new LinkedList(); classQueue.addFirst(sourceClass); @@ -355,7 +364,9 @@ public class GenericConversionService implements ConversionService { return (Converter) converters.get(targetClass); } - private SuperConverter findRegisteredSuperConverter(Class sourceClass, Class targetClass) { + private SuperConverter findRegisteredSuperConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + Class sourceClass = sourceType.getWrapperTypeIfPrimitive(); + Class targetClass = targetType.getWrapperTypeIfPrimitive(); if (sourceClass.isInterface()) { LinkedList classQueue = new LinkedList(); classQueue.addFirst(sourceClass); @@ -441,7 +452,7 @@ public class GenericConversionService implements ConversionService { } public ConversionExecutor getElementConverter(Class sourceElementType, Class targetElementType) { - return getConversionExecutor(new TypeDescriptor(sourceElementType), new TypeDescriptor(targetElementType)); + return getConversionExecutor(TypeDescriptor.valueOf(sourceElementType), TypeDescriptor.valueOf(targetElementType)); } } \ No newline at end of file diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/service/NoOpConverter.java b/org.springframework.core/src/main/java/org/springframework/core/convert/service/NoOpConverter.java index b9f8df8780..dab3ee15ef 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/service/NoOpConverter.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/service/NoOpConverter.java @@ -20,6 +20,12 @@ import org.springframework.core.convert.converter.Converter; @SuppressWarnings("unchecked") class NoOpConverter implements Converter { + public static final Converter INSTANCE = new NoOpConverter(); + + private NoOpConverter() { + + } + public Object convert(Object source) throws Exception { return source; } diff --git a/org.springframework.core/src/test/java/org/springframework/core/convert/service/GenericConversionServiceTests.java b/org.springframework.core/src/test/java/org/springframework/core/convert/service/GenericConversionServiceTests.java index f5c3836c72..e27c6cc6c3 100644 --- a/org.springframework.core/src/test/java/org/springframework/core/convert/service/GenericConversionServiceTests.java +++ b/org.springframework.core/src/test/java/org/springframework/core/convert/service/GenericConversionServiceTests.java @@ -24,6 +24,7 @@ import java.util.List; import junit.framework.TestCase; +import org.junit.Ignore; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionExecutionException; import org.springframework.core.convert.ConversionExecutor; @@ -35,25 +36,18 @@ import org.springframework.core.convert.converter.NumberToNumber; import org.springframework.core.convert.converter.StringToEnum; import org.springframework.core.convert.converter.StringToInteger; -import org.junit.Ignore; - -@Ignore public class GenericConversionServiceTests extends TestCase { private GenericConversionService service = new GenericConversionService(); - private TypedValue value(Object obj) { - return new TypedValue(obj, new TypeDescriptor(obj.getClass())); - } - - private TypeDescriptor type(Class clazz) { - return new TypeDescriptor(clazz); - } - public void testExecuteConversion() { service.addConverter(new StringToInteger()); assertEquals(new Integer(3), service.executeConversion(value("3"), type(Integer.class))); } + + public void testExecuteConversionNullSource() { + assertEquals(null, service.executeConversion(TypedValue.NULL, type(Integer.class))); + } public void testConverterConversionForwardIndex() { service.addConverter(new StringToInteger()); @@ -77,6 +71,23 @@ public class GenericConversionServiceTests extends TestCase { } } + public void testAddConverterNoSourceTargetClassInfoAvailable() { + try { + service.addConverter(new Converter() { + public Object convert(Object source) throws Exception { + return source; + } + + public Object convertBack(Object target) throws Exception { + return target; + } + }); + fail("Should have failed"); + } catch (IllegalArgumentException e) { + + } + } + public void testConversionCompatibleTypes() { String source = "foo"; assertSame(source, service.getConversionExecutor(type(String.class), type(String.class)).execute(source)); @@ -165,8 +176,11 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("3", result.get(2)); } - public void testConversionArrayToListGenericTypeConversion() { - ConversionExecutor executor = service.getConversionExecutor(type(String[].class), type(List.class)); + public List genericList = new ArrayList(); + + public void testConversionArrayToListGenericTypeConversion() throws Exception { + service.addConverter(new StringToInteger()); + ConversionExecutor executor = service.getConversionExecutor(type(String[].class), new TypeDescriptor(getClass().getDeclaredField("genericList"))); List result = (List) executor.execute(new String[] { "1", "2", "3" }); assertEquals(new Integer("1"), result.get(0)); assertEquals(new Integer("2"), result.get(1)); @@ -214,6 +228,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals(new Integer(3), result[2]); } + @Ignore public void testConversionObjectToArray() { ConversionExecutor executor = service.getConversionExecutor(type(String.class), type(String[].class)); String[] result = (String[]) executor.execute("1,2,3"); @@ -221,6 +236,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("1,2,3", result[0]); } + @Ignore public void testConversionObjectToArrayWithElementConversion() { service.addConverter(new StringToInteger()); ConversionExecutor executor = service.getConversionExecutor(type(String.class), type(Integer[].class)); @@ -288,29 +304,14 @@ public class GenericConversionServiceTests extends TestCase { } } - public void testAddConverterNoSourceTargetClassInfoAvailable() { - try { - service.addConverter(new Converter() { - public Object convert(Object source) throws Exception { - return source; - } - - public Object convertBack(Object target) throws Exception { - return target; - } - }); - fail("Should have failed"); - } catch (IllegalArgumentException e) { - - } - } - + @Ignore public void testCustomConverterConversionForwardIndex() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(String.class), type(Principal.class)); assertEquals("keith", ((Principal) executor.execute("keith")).getName()); } + @Ignore public void testCustomConverterConversionReverseIndex() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(Principal.class), type(String.class)); @@ -321,12 +322,14 @@ public class GenericConversionServiceTests extends TestCase { })); } + @Ignore public void testCustomConverterConversionForSameType() { service.addConverter("trimmer", new Trimmer()); ConversionExecutor executor = service.getConversionExecutor("trimmer", type(String.class), type(String.class)); assertEquals("a string", executor.execute("a string ")); } + @Ignore public void testCustomConverterLookupNotCompatibleSource() { service.addConverter("trimmer", new Trimmer()); try { @@ -337,6 +340,7 @@ public class GenericConversionServiceTests extends TestCase { } } + @Ignore public void testCustomConverterLookupNotCompatibleTarget() { service.addConverter("trimmer", new Trimmer()); try { @@ -346,6 +350,7 @@ public class GenericConversionServiceTests extends TestCase { } } + @Ignore public void testCustomConverterLookupNotCompatibleTargetReverse() { service.addConverter("princy", new CustomTwoWayConverter()); try { @@ -355,6 +360,7 @@ public class GenericConversionServiceTests extends TestCase { } } + @Ignore public void testCustomConverterConversionArrayToArray() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(String[].class), type(Principal[].class)); @@ -363,6 +369,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("princy2", p[1].getName()); } + @Ignore public void testCustomConverterConversionArrayToArrayReverse() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(Principal[].class), type(String[].class)); @@ -381,6 +388,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("princy2", p[1]); } + @Ignore public void testCustomConverterLookupArrayToArrayBogusSource() { service.addConverter("princy", new CustomTwoWayConverter()); try { @@ -390,6 +398,7 @@ public class GenericConversionServiceTests extends TestCase { } } + @Ignore public void testCustomConverterLookupArrayToArrayBogusTarget() { service.addConverter("princy", new CustomTwoWayConverter()); try { @@ -399,6 +408,7 @@ public class GenericConversionServiceTests extends TestCase { } } + @Ignore public void testCustomConverterConversionArrayToCollection() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(String[].class), type(List.class)); @@ -407,6 +417,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("princy2", ((Principal) list.get(1)).getName()); } + @Ignore public void testCustomConverterConversionArrayToCollectionReverse() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(Principal[].class), type(List.class)); @@ -425,6 +436,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("princy2", p.get(1)); } + @Ignore public void testCustomConverterLookupArrayToCollectionBogusSource() { service.addConverter("princy", new CustomTwoWayConverter()); try { @@ -435,6 +447,7 @@ public class GenericConversionServiceTests extends TestCase { } } + @Ignore public void testCustomConverterLookupCollectionToArray() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(List.class), type(Principal[].class)); @@ -446,6 +459,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("princy2", p[1].getName()); } + @Ignore public void testCustomConverterLookupCollectionToArrayReverse() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(List.class), type(String[].class)); @@ -467,7 +481,8 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("princy2", p[1]); } - public void testtestCustomConverterLookupCollectionToArrayBogusTarget() { + @Ignore + public void testCustomConverterLookupCollectionToArrayBogusTarget() { service.addConverter("princy", new CustomTwoWayConverter()); try { service.getConversionExecutor("princy", type(List.class), type(Integer[].class)); @@ -477,6 +492,7 @@ public class GenericConversionServiceTests extends TestCase { } } + @Ignore public void testCustomConverterConversionObjectToArray() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(String.class), type(Principal[].class)); @@ -484,6 +500,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("princy1", p[0].getName()); } + @Ignore public void testCustomConverterConversionObjectToArrayReverse() { service.addConverter("princy", new CustomTwoWayConverter()); ConversionExecutor executor = service.getConversionExecutor("princy", type(Principal.class), type(String[].class)); @@ -496,6 +513,7 @@ public class GenericConversionServiceTests extends TestCase { assertEquals("princy1", p[0]); } + @Ignore public void testCustomConverterLookupObjectToArrayBogusSource() { service.addConverter("princy", new CustomTwoWayConverter()); try { @@ -540,4 +558,12 @@ public class GenericConversionServiceTests extends TestCase { //assertEquals(FooEnum.BAR, service.executeConversion("BAR", FooEnum.class)); } + private TypedValue value(Object obj) { + return new TypedValue(obj); + } + + private TypeDescriptor type(Class clazz) { + return TypeDescriptor.valueOf(clazz); + } + } \ No newline at end of file