SPR-6179, additional mapper test cases

git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@2047 50f2f4bb-b051-0410-bef5-90022cba6387
This commit is contained in:
Keith Donald 2009-10-04 04:13:27 +00:00
parent 1a53314e92
commit 4af6fd7e0d
9 changed files with 234 additions and 106 deletions

View File

@ -27,8 +27,9 @@ public interface Mapper<S, T> {
* Map the source to the target. * Map the source to the target.
* @param source the source to map from * @param source the source to map from
* @param target the target to map to * @param target the target to map to
* @return the mapped target object
* @throws MappingException if the mapping process failed * @throws MappingException if the mapping process failed
*/ */
void map(S source, T target); Object map(S source, T target);
} }

View File

@ -60,11 +60,12 @@ public class MappingException extends RuntimeException {
public void printStackTrace(PrintStream ps) { public void printStackTrace(PrintStream ps) {
super.printStackTrace(ps); super.printStackTrace(ps);
synchronized (ps) { synchronized (ps) {
ps.println("Failure cause traces:"); ps.println();
ps.println("Mapping Failure Traces:");
int i = 1; int i = 1;
for (Iterator<MappingFailure> it = this.mappingFailures.iterator(); it.hasNext(); i++) { for (Iterator<MappingFailure> it = this.mappingFailures.iterator(); it.hasNext(); i++) {
MappingFailure failure = it.next(); MappingFailure failure = it.next();
ps.println("- MappingFailure #" + i + " Cause: "); ps.println("- MappingFailure #" + i + ":");
Throwable t = failure.getCause(); Throwable t = failure.getCause();
if (t != null) { if (t != null) {
t.printStackTrace(ps); t.printStackTrace(ps);
@ -79,11 +80,12 @@ public class MappingException extends RuntimeException {
public void printStackTrace(PrintWriter pw) { public void printStackTrace(PrintWriter pw) {
super.printStackTrace(pw); super.printStackTrace(pw);
synchronized (pw) { synchronized (pw) {
pw.println("Failure cause traces:"); pw.println();
pw.println("Mapping Failure Traces:");
int i = 1; int i = 1;
for (Iterator<MappingFailure> it = this.mappingFailures.iterator(); it.hasNext(); i++) { for (Iterator<MappingFailure> it = this.mappingFailures.iterator(); it.hasNext(); i++) {
MappingFailure failure = it.next(); MappingFailure failure = it.next();
pw.println("- MappingFailure #" + i + " Cause: "); pw.println("- MappingFailure #" + i + ":");
Throwable t = failure.getCause(); Throwable t = failure.getCause();
if (t != null) { if (t != null) {
t.printStackTrace(pw); t.printStackTrace(pw);

View File

@ -25,6 +25,7 @@ import java.util.Set;
import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.ParseException; import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
@ -60,27 +61,27 @@ public class SpelMapper implements Mapper<Object, Object> {
this.autoMappingEnabled = autoMappingEnabled; this.autoMappingEnabled = autoMappingEnabled;
} }
public MappingConfiguration addMapping(String expression) { public MappingConfiguration addMapping(String fieldExpression) {
return addMapping(expression, expression); return addMapping(fieldExpression, fieldExpression);
} }
public ConverterRegistry getConverterRegistry() { public ConverterRegistry getConverterRegistry() {
return conversionService; return conversionService;
} }
public MappingConfiguration addMapping(String sourceExpression, String targetExpression) { public MappingConfiguration addMapping(String sourceFieldExpression, String targetFieldExpression) {
Expression sourceExp; Expression sourceExp;
try { try {
sourceExp = sourceExpressionParser.parseExpression(sourceExpression); sourceExp = sourceExpressionParser.parseExpression(sourceFieldExpression);
} catch (ParseException e) { } catch (ParseException e) {
throw new IllegalArgumentException("The mapping source '" + sourceExpression throw new IllegalArgumentException("The mapping source '" + sourceFieldExpression
+ "' is not a parseable value expression", e); + "' is not a parseable value expression", e);
} }
Expression targetExp; Expression targetExp;
try { try {
targetExp = targetExpressionParser.parseExpression(targetExpression); targetExp = targetExpressionParser.parseExpression(targetFieldExpression);
} catch (ParseException e) { } catch (ParseException e) {
throw new IllegalArgumentException("The mapping target '" + targetExpression throw new IllegalArgumentException("The mapping target '" + targetFieldExpression
+ "' is not a parseable property expression", e); + "' is not a parseable property expression", e);
} }
Mapping mapping = new Mapping(sourceExp, targetExp); Mapping mapping = new Mapping(sourceExp, targetExp);
@ -88,33 +89,33 @@ public class SpelMapper implements Mapper<Object, Object> {
return mapping; return mapping;
} }
public void map(Object source, Object target) { public Object map(Object source, Object target) {
EvaluationContext sourceContext = getMappingContext(source); EvaluationContext sourceContext = getMappingContext(source);
EvaluationContext targetContext = getMappingContext(target); EvaluationContext targetContext = getMappingContext(target);
List<MappingFailure> failures = new LinkedList<MappingFailure>(); List<MappingFailure> failures = new LinkedList<MappingFailure>();
for (Mapping mapping : this.mappings) { for (Mapping mapping : this.mappings) {
mapping.map(sourceContext, targetContext, failures); mapping.map(sourceContext, targetContext, failures);
} }
Set<Mapping> autoMappings = getAutoMappings(source, target); Set<Mapping> autoMappings = getAutoMappings(sourceContext, targetContext);
for (Mapping mapping : autoMappings) { for (Mapping mapping : autoMappings) {
mapping.map(sourceContext, targetContext, failures); mapping.map(sourceContext, targetContext, failures);
} }
if (!failures.isEmpty()) { if (!failures.isEmpty()) {
throw new MappingException(failures); throw new MappingException(failures);
} }
return target;
} }
private EvaluationContext getMappingContext(Object object) { private EvaluationContext getMappingContext(Object object) {
return mappableTypeFactory.getMappableType(object).getEvaluationContext(object, this.conversionService); return mappableTypeFactory.getMappableType(object).getEvaluationContext(object, this.conversionService);
} }
private Set<Mapping> getAutoMappings(Object source, Object target) { private Set<Mapping> getAutoMappings(EvaluationContext sourceContext, EvaluationContext targetContext) {
if (this.autoMappingEnabled) { if (this.autoMappingEnabled) {
Set<Mapping> autoMappings = new LinkedHashSet<Mapping>(); Set<Mapping> autoMappings = new LinkedHashSet<Mapping>();
Set<String> sourceFields = getMappableFields(source); Set<String> sourceFields = getMappableFields(sourceContext.getRootObject().getValue());
Set<String> targetFields = getMappableFields(target);
for (String field : sourceFields) { for (String field : sourceFields) {
if (!explicitlyMapped(field) && targetFields.contains(field)) { if (!explicitlyMapped(field)) {
Expression sourceExpression; Expression sourceExpression;
Expression targetExpression; Expression targetExpression;
try { try {
@ -129,8 +130,13 @@ public class SpelMapper implements Mapper<Object, Object> {
throw new IllegalArgumentException("The mapping target '" + field throw new IllegalArgumentException("The mapping target '" + field
+ "' is not a parseable value expression", e); + "' is not a parseable value expression", e);
} }
Mapping mapping = new Mapping(sourceExpression, targetExpression); try {
autoMappings.add(mapping); if (targetExpression.isWritable(targetContext)) {
autoMappings.add(new Mapping(sourceExpression, targetExpression));
}
} catch (EvaluationException e) {
}
} }
} }
return autoMappings; return autoMappings;
@ -145,7 +151,7 @@ public class SpelMapper implements Mapper<Object, Object> {
private boolean explicitlyMapped(String field) { private boolean explicitlyMapped(String field) {
for (Mapping mapping : this.mappings) { for (Mapping mapping : this.mappings) {
if (mapping.getSourceExpressionString().equals(field)) { if (mapping.getSourceExpressionString().startsWith(field)) {
return true; return true;
} }
} }

View File

@ -1,6 +1,7 @@
package org.springframework.mapping.support; package org.springframework.mapping.support;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -31,15 +32,14 @@ public class SpelMapperTests {
@Test @Test
public void mapExplicit() throws MappingException { public void mapExplicit() throws MappingException {
mapper.setAutoMappingEnabled(false);
mapper.addMapping("name");
Map<String, Object> source = new HashMap<String, Object>(); Map<String, Object> source = new HashMap<String, Object>();
source.put("name", "Keith"); source.put("name", "Keith");
source.put("age", 31); source.put("age", 31);
Person target = new Person(); Person target = new Person();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("name");
mapper.map(source, target); mapper.map(source, target);
assertEquals("Keith", target.name); assertEquals("Keith", target.name);
@ -48,8 +48,6 @@ public class SpelMapperTests {
@Test @Test
public void mapAutomaticWithExplictOverrides() { public void mapAutomaticWithExplictOverrides() {
mapper.addMapping("test", "age");
Map<String, Object> source = new HashMap<String, Object>(); Map<String, Object> source = new HashMap<String, Object>();
source.put("name", "Keith"); source.put("name", "Keith");
source.put("test", "3"); source.put("test", "3");
@ -57,6 +55,7 @@ public class SpelMapperTests {
Person target = new Person(); Person target = new Person();
mapper.addMapping("test", "age");
mapper.map(source, target); mapper.map(source, target);
assertEquals("Keith", target.name); assertEquals("Keith", target.name);
@ -65,15 +64,29 @@ public class SpelMapperTests {
} }
@Test @Test
public void mapSameSourceFieldToMultipleTargets() { public void mapAutomaticIgnoreUnknownField() {
mapper.addMapping("test", "name"); Map<String, Object> source = new HashMap<String, Object>();
mapper.addMapping("test", "favoriteSport"); source.put("name", "Keith");
source.put("age", 31);
source.put("unknown", "foo");
Person target = new Person();
mapper.map(source, target);
assertEquals("Keith", target.name);
assertEquals(31, target.age);
}
@Test
public void mapSameSourceFieldToMultipleTargets() {
Map<String, Object> source = new HashMap<String, Object>(); Map<String, Object> source = new HashMap<String, Object>();
source.put("test", "FOOTBALL"); source.put("test", "FOOTBALL");
Person target = new Person(); Person target = new Person();
mapper.addMapping("test", "name");
mapper.addMapping("test", "favoriteSport");
mapper.map(source, target); mapper.map(source, target);
assertEquals("FOOTBALL", target.name); assertEquals("FOOTBALL", target.name);
@ -92,7 +105,6 @@ public class SpelMapperTests {
mapper.addMapping("fullName", "name"); mapper.addMapping("fullName", "name");
mapper.addMapping("sport", "favoriteSport"); mapper.addMapping("sport", "favoriteSport");
mapper.map(source, target); mapper.map(source, target);
assertEquals("Keith Donald", target.name); assertEquals("Keith Donald", target.name);
@ -100,24 +112,46 @@ public class SpelMapperTests {
assertEquals(Sport.FOOTBALL, target.favoriteSport); assertEquals(Sport.FOOTBALL, target.favoriteSport);
} }
@Test
public void mapBeanDeep() {
PersonDto source = new PersonDto();
source.age = "0";
NestedDto nested = new NestedDto();
nested.foo = "bar";
source.setNested(nested);
Person target = new Person();
mapper.addMapping("nested.foo", "nested.foo");
mapper.map(source, target);
assertEquals("bar", target.nested.foo);
}
@Test @Test
public void mapBeanNested() { public void mapBeanNested() {
PersonDto source = new PersonDto(); PersonDto source = new PersonDto();
source.setFullName("Keith Donald"); source.setFullName("Keith Donald");
source.setAge("31"); source.setAge("31");
source.setSport("FOOTBALL"); source.setSport("FOOTBALL");
NestedDto nested = new NestedDto();
nested.foo = "bar";
source.setNested(nested);
Person target = new Person(); Person target = new Person();
mapper.addMapping("fullName", "nested.fullName"); mapper.addMapping("fullName", "name");
mapper.addMapping("age", "nested.age"); mapper.addMapping("sport", "favoriteSport");
mapper.addMapping("sport", "nested.sport"); mapper.addMapping("nested", "nested").setConverter(new Converter<NestedDto, Nested>() {
public Nested convert(NestedDto source) {
return (Nested) new SpelMapper().map(source, new Nested());
}
});
mapper.map(source, target); mapper.map(source, target);
assertEquals("Keith Donald", target.getNested().getFullName()); assertEquals("Keith Donald", target.name);
assertEquals("31", target.nested.age); assertEquals(31, target.age);
assertEquals("FOOTBALL", target.nested.sport); assertEquals(Sport.FOOTBALL, target.favoriteSport);
} }
@Test @Test
@ -132,13 +166,30 @@ public class SpelMapperTests {
mapper.setAutoMappingEnabled(false); mapper.setAutoMappingEnabled(false);
mapper.addMapping("sports", "favoriteSports"); mapper.addMapping("sports", "favoriteSports");
mapper.map(source, target); mapper.map(source, target);
assertEquals(Sport.FOOTBALL, target.favoriteSports.get(0)); assertEquals(Sport.FOOTBALL, target.favoriteSports.get(0));
assertEquals(Sport.BASKETBALL, target.favoriteSports.get(1)); assertEquals(Sport.BASKETBALL, target.favoriteSports.get(1));
} }
@Test
public void mapListFlatten() {
PersonDto source = new PersonDto();
List<String> sports = new ArrayList<String>();
sports.add("FOOTBALL");
sports.add("BASKETBALL");
source.setSports(sports);
Person target = new Person();
mapper.setAutoMappingEnabled(false);
mapper.addMapping("sports[0]", "favoriteSport");
mapper.map(source, target);
assertEquals(Sport.FOOTBALL, target.favoriteSport);
assertNull(target.favoriteSports);
}
@Test @Test
public void mapMap() { public void mapMap() {
PersonDto source = new PersonDto(); PersonDto source = new PersonDto();
@ -187,7 +238,7 @@ public class SpelMapperTests {
public void mapFailure() { public void mapFailure() {
Map<String, Object> source = new HashMap<String, Object>(); Map<String, Object> source = new HashMap<String, Object>();
source.put("name", "Keith"); source.put("name", "Keith");
source.put("age", "bogus"); source.put("age", "invalid");
Person target = new Person(); Person target = new Person();
try { try {
mapper.map(source, target); mapper.map(source, target);
@ -208,7 +259,7 @@ public class SpelMapperTests {
private Map<String, String> friendRankings; private Map<String, String> friendRankings;
private NestedDto nestedDto; private NestedDto nested;
public String getFullName() { public String getFullName() {
return fullName; return fullName;
@ -250,12 +301,12 @@ public class SpelMapperTests {
this.friendRankings = friendRankings; this.friendRankings = friendRankings;
} }
public NestedDto getNestedDto() { public NestedDto getNested() {
return nestedDto; return nested;
} }
public void setNestedDto(NestedDto nestedDto) { public void setNested(NestedDto nested) {
this.nestedDto = nestedDto; this.nested = nested;
} }
} }
@ -277,7 +328,7 @@ public class SpelMapperTests {
private Sport favoriteSport; private Sport favoriteSport;
private PersonDto nested; private Nested nested;
// private Person cyclic; // private Person cyclic;
@ -317,11 +368,11 @@ public class SpelMapperTests {
this.favoriteSport = favoriteSport; this.favoriteSport = favoriteSport;
} }
public PersonDto getNested() { public Nested getNested() {
return nested; return nested;
} }
public void setNested(PersonDto nested) { public void setNested(Nested nested) {
this.nested = nested; this.nested = nested;
} }
@ -364,6 +415,20 @@ public class SpelMapperTests {
} }
} }
public static class Nested {
private String foo;
public String getFoo() {
return foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
}
public enum Sport { public enum Sport {
FOOTBALL, BASKETBALL FOOTBALL, BASKETBALL
} }

View File

@ -159,6 +159,14 @@ public class TypeDescriptor {
} }
} }
/**
* Is this type a primitive type?
*/
public boolean isPrimitive() {
Class<?> type = getType();
return (type != null && type.isPrimitive());
}
/** /**
* Is this type an array type? * Is this type an array type?
*/ */

View File

@ -16,6 +16,8 @@
package org.springframework.core.convert.support; package org.springframework.core.convert.support;
import java.util.Collection;
import java.util.Map;
/** /**
* Default implementation of a conversion service. Will automatically register <i>from string</i> * Default implementation of a conversion service. Will automatically register <i>from string</i>
@ -31,6 +33,21 @@ public class DefaultConversionService extends GenericConversionService {
* Create a new default conversion service, installing the default converters. * Create a new default conversion service, installing the default converters.
*/ */
public DefaultConversionService() { public DefaultConversionService() {
addGenericConverter(Object[].class, Object[].class, new ArrayToArrayConverter(this));
addGenericConverter(Object[].class, Collection.class, new ArrayToCollectionConverter(this));
addGenericConverter(Object[].class, Map.class, new ArrayToMapConverter(this));
addGenericConverter(Object[].class, Object.class, new ArrayToObjectConverter(this));
addGenericConverter(Collection.class, Collection.class, new CollectionToCollectionConverter(this));
addGenericConverter(Collection.class, Object[].class, new CollectionToArrayConverter(this));
addGenericConverter(Collection.class, Map.class, new CollectionToMapConverter(this));
addGenericConverter(Collection.class, Object.class, new CollectionToObjectConverter(this));
addGenericConverter(Map.class, Map.class, new MapToMapConverter(this));
addGenericConverter(Map.class, Object[].class, new MapToArrayConverter(this));
addGenericConverter(Map.class, Collection.class, new MapToCollectionConverter(this));
addGenericConverter(Map.class, Object.class, new MapToObjectConverter(this));
addGenericConverter(Object.class, Object[].class, new ObjectToArrayConverter(this));
addGenericConverter(Object.class, Collection.class, new ObjectToCollectionConverter(this));
addGenericConverter(Object.class, Map.class, new ObjectToMapConverter(this));
addConverter(new StringToBooleanConverter()); addConverter(new StringToBooleanConverter());
addConverter(new StringToCharacterConverter()); addConverter(new StringToCharacterConverter());
addConverter(new StringToLocaleConverter()); addConverter(new StringToLocaleConverter());

View File

@ -16,6 +16,8 @@
package org.springframework.core.convert.support; package org.springframework.core.convert.support;
import static org.springframework.core.convert.support.ConversionUtils.invokeConverter;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -25,6 +27,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import org.springframework.core.GenericTypeResolver; import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
@ -32,13 +35,11 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.ConverterInfo; import org.springframework.core.convert.converter.ConverterInfo;
import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.converter.ConverterRegistry;
import static org.springframework.core.convert.support.ConversionUtils.*;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
/** /**
* Base implementation of a conversion service. * Base ConversionService implementation suitable for use in most environments.
*
* @author Keith Donald * @author Keith Donald
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 3.0 * @since 3.0
@ -63,8 +64,12 @@ public class GenericConversionService implements ConversionService, ConverterReg
} }
}; };
/**
* Create a new GenericConversionService.
* Generic converters for Collection types are registered.
*/
public GenericConversionService() { public GenericConversionService() {
// TODO should these only be registered in DefaultConversionService?
addGenericConverter(Object[].class, Object[].class, new ArrayToArrayConverter(this)); addGenericConverter(Object[].class, Object[].class, new ArrayToArrayConverter(this));
addGenericConverter(Object[].class, Collection.class, new ArrayToCollectionConverter(this)); addGenericConverter(Object[].class, Collection.class, new ArrayToCollectionConverter(this));
addGenericConverter(Object[].class, Map.class, new ArrayToMapConverter(this)); addGenericConverter(Object[].class, Map.class, new ArrayToMapConverter(this));
@ -82,7 +87,6 @@ public class GenericConversionService implements ConversionService, ConverterReg
addGenericConverter(Object.class, Map.class, new ObjectToMapConverter(this)); addGenericConverter(Object.class, Map.class, new ObjectToMapConverter(this));
} }
/** /**
* Registers the converters in the set provided. * Registers the converters in the set provided.
* JavaBean-friendly alternative to calling {@link #addConverter(Converter)}. * JavaBean-friendly alternative to calling {@link #addConverter(Converter)}.
@ -113,13 +117,12 @@ public class GenericConversionService implements ConversionService, ConverterReg
} }
/** /**
* Returns the parent of this conversion service. Could be null. * Returns the parent of this conversion service. May be null.
*/ */
public ConversionService getParent() { public ConversionService getParent() {
return this.parent; return this.parent;
} }
// implementing ConverterRegistry // implementing ConverterRegistry
public void addConverter(Converter<?, ?> converter) { public void addConverter(Converter<?, ?> converter) {
@ -130,7 +133,7 @@ public class GenericConversionService implements ConversionService, ConverterReg
} }
Class sourceType = typeInfo[0]; Class sourceType = typeInfo[0];
Class targetType = typeInfo[1]; Class targetType = typeInfo[1];
getSourceMap(sourceType).put(targetType, new ConverterGenericConverter(converter)); getSourceMap(sourceType).put(targetType, new ConverterAdapter(converter));
} }
public void addConverterFactory(ConverterFactory<?, ?> converterFactory) { public void addConverterFactory(ConverterFactory<?, ?> converterFactory) {
@ -141,43 +144,37 @@ public class GenericConversionService implements ConversionService, ConverterReg
} }
Class sourceType = typeInfo[0]; Class sourceType = typeInfo[0];
Class targetType = typeInfo[1]; Class targetType = typeInfo[1];
getSourceMap(sourceType).put(targetType, new ConverterFactoryGenericConverter(converterFactory)); getSourceMap(sourceType).put(targetType, new ConverterFactoryAdapter(converterFactory));
} }
public void removeConvertible(Class<?> sourceType, Class<?> targetType) { public void removeConvertible(Class<?> sourceType, Class<?> targetType) {
getSourceMap(sourceType).remove(targetType); getSourceMap(sourceType).remove(targetType);
} }
// implementing ConversionService // implementing ConversionService
public boolean canConvert(Class<?> sourceType, Class<?> targetType) { public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
return canConvert(TypeDescriptor.valueOf(sourceType), TypeDescriptor.valueOf(targetType)); return canConvert(TypeDescriptor.valueOf(sourceType), TypeDescriptor.valueOf(targetType));
} }
@SuppressWarnings("unchecked")
public <T> T convert(Object source, Class<T> targetType) { public <T> T convert(Object source, Class<T> targetType) {
Assert.notNull(targetType, "The targetType to convert to is required");
return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType)); return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
} }
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(sourceType, "The sourceType to convert from is required"); assertNotNull(sourceType, targetType);
Assert.notNull(targetType, "The targetType to convert to is required"); if (sourceType == TypeDescriptor.NULL || targetType == TypeDescriptor.NULL) {
if (targetType == TypeDescriptor.NULL) {
return true; return true;
} }
return getConverter(sourceType, targetType) != null; return getConverter(sourceType, targetType) != null;
} }
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(sourceType, "The sourceType to convert to is required"); assertNotNull(sourceType, targetType);
Assert.notNull(targetType, "The targetType to convert to is required"); if (sourceType == TypeDescriptor.NULL) {
if (source == null) { Assert.isTrue(source == null, "The source must be null if sourceType == TypeDescriptor.NULL");
return convertNull(sourceType, targetType); return convertNullSource(sourceType, targetType);
} }
Assert.isTrue(sourceType != TypeDescriptor.NULL,
"The source TypeDescriptor must not be TypeDescriptor.NULL when source != null");
if (targetType == TypeDescriptor.NULL) { if (targetType == TypeDescriptor.NULL) {
return null; return null;
} }
@ -188,7 +185,6 @@ public class GenericConversionService implements ConversionService, ConverterReg
return invokeConverter(converter, source, sourceType, targetType); return invokeConverter(converter, source, sourceType, targetType);
} }
// subclassing hooks // subclassing hooks
/** /**
@ -197,19 +193,25 @@ public class GenericConversionService implements ConversionService, ConverterReg
* @param targetType the target type to convert to * @param targetType the target type to convert to
* @param converter the generic converter. * @param converter the generic converter.
*/ */
// TODO should this be public?
protected void addGenericConverter(Class<?> sourceType, Class<?> targetType, GenericConverter converter) { protected void addGenericConverter(Class<?> sourceType, Class<?> targetType, GenericConverter converter) {
getSourceMap(sourceType).put(targetType, converter); getSourceMap(sourceType).put(targetType, converter);
} }
/** /**
* Hook method to convert a null value. * Hook method to convert a null source.
* Default implementation simply returns <code>null</code>. * Default implementation returns <code>null</code>.
* Subclasses may override to return a custom null objects for specific target types. * Throws a {@link ConversionFailedException} if the targetType is a primitive type, as null cannot be assigned to a primitive type.
* @param sourceType the sourceType * Subclasses may override to return custom null objects for specific target types.
* @param targetType the tagetType * @param sourceType the sourceType to convert from
* @return the null object * @param targetType the targetType to convert to
* @return the converted null object
*/ */
protected Object convertNull(TypeDescriptor sourceType, TypeDescriptor targetType) { protected Object convertNullSource(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (targetType.isPrimitive()) {
throw new ConversionFailedException(sourceType, targetType, null, new IllegalArgumentException(
"A null value cannot be assigned to a primitive type"));
}
return null; return null;
} }
@ -228,23 +230,24 @@ public class GenericConversionService implements ConversionService, ConverterReg
GenericConverter converter = findConverterByClassPair(sourceType.getObjectType(), targetType.getObjectType()); GenericConverter converter = findConverterByClassPair(sourceType.getObjectType(), targetType.getObjectType());
if (converter != null) { if (converter != null) {
return converter; return converter;
} } else if (this.parent != null && this.parent.canConvert(sourceType, targetType)) {
else if (this.parent != null && this.parent.canConvert(sourceType, targetType)) {
return this.parentConverterAdapter; return this.parentConverterAdapter;
} } else {
else {
if (sourceType.isAssignableTo(targetType)) { if (sourceType.isAssignableTo(targetType)) {
return NO_OP_CONVERTER; return NO_OP_CONVERTER;
} } else {
else {
return null; return null;
} }
} }
} }
// internal helpers // internal helpers
private void assertNotNull(TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(sourceType, "The sourceType to convert to is required");
Assert.notNull(targetType, "The targetType to convert to is required");
}
private Class[] getRequiredTypeInfo(Object converter, Class genericIfc) { private Class[] getRequiredTypeInfo(Object converter, Class genericIfc) {
Class[] typeInfo = new Class[2]; Class[] typeInfo = new Class[2];
if (converter instanceof ConverterInfo) { if (converter instanceof ConverterInfo) {
@ -252,8 +255,7 @@ public class GenericConversionService implements ConversionService, ConverterReg
typeInfo[0] = info.getSourceType(); typeInfo[0] = info.getSourceType();
typeInfo[1] = info.getTargetType(); typeInfo[1] = info.getTargetType();
return typeInfo; return typeInfo;
} } else {
else {
return GenericTypeResolver.resolveTypeArguments(converter.getClass(), genericIfc); return GenericTypeResolver.resolveTypeArguments(converter.getClass(), genericIfc);
} }
} }
@ -276,8 +278,7 @@ public class GenericConversionService implements ConversionService, ConverterReg
} }
Map<Class, GenericConverter> objectConverters = getConvertersForSource(Object.class); Map<Class, GenericConverter> objectConverters = getConvertersForSource(Object.class);
return getConverter(objectConverters, targetType); return getConverter(objectConverters, targetType);
} } else {
else {
LinkedList<Class> classQueue = new LinkedList<Class>(); LinkedList<Class> classQueue = new LinkedList<Class>();
classQueue.addFirst(sourceType); classQueue.addFirst(sourceType);
while (!classQueue.isEmpty()) { while (!classQueue.isEmpty()) {
@ -292,8 +293,7 @@ public class GenericConversionService implements ConversionService, ConverterReg
if (componentType.getSuperclass() != null) { if (componentType.getSuperclass() != null) {
classQueue.addFirst(Array.newInstance(componentType.getSuperclass(), 0).getClass()); classQueue.addFirst(Array.newInstance(componentType.getSuperclass(), 0).getClass());
} }
} } else {
else {
if (currentClass.getSuperclass() != null) { if (currentClass.getSuperclass() != null) {
classQueue.addFirst(currentClass.getSuperclass()); classQueue.addFirst(currentClass.getSuperclass());
} }
@ -340,8 +340,7 @@ public class GenericConversionService implements ConversionService, ConverterReg
} }
} }
return converters.get(Object.class); return converters.get(Object.class);
} } else {
else {
LinkedList<Class> classQueue = new LinkedList<Class>(); LinkedList<Class> classQueue = new LinkedList<Class>();
classQueue.addFirst(targetType); classQueue.addFirst(targetType);
while (!classQueue.isEmpty()) { while (!classQueue.isEmpty()) {
@ -355,8 +354,7 @@ public class GenericConversionService implements ConversionService, ConverterReg
if (componentType.getSuperclass() != null) { if (componentType.getSuperclass() != null) {
classQueue.addFirst(Array.newInstance(componentType.getSuperclass(), 0).getClass()); classQueue.addFirst(Array.newInstance(componentType.getSuperclass(), 0).getClass());
} }
} } else {
else {
if (currentClass.getSuperclass() != null) { if (currentClass.getSuperclass() != null) {
classQueue.addFirst(currentClass.getSuperclass()); classQueue.addFirst(currentClass.getSuperclass());
} }
@ -370,4 +368,36 @@ public class GenericConversionService implements ConversionService, ConverterReg
} }
} }
private final class ConverterAdapter implements GenericConverter {
private final Converter converter;
public ConverterAdapter(Converter converter) {
this.converter = converter;
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return convertNullSource(sourceType, targetType);
}
return this.converter.convert(source);
}
}
private final class ConverterFactoryAdapter implements GenericConverter {
private final ConverterFactory converterFactory;
public ConverterFactoryAdapter(ConverterFactory converterFactory) {
this.converterFactory = converterFactory;
}
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return convertNullSource(sourceType, targetType);
}
return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
}
}
} }

View File

@ -91,14 +91,13 @@ public class GenericConversionServiceTests {
assertNull(conversionService.convert(null, Integer.class)); assertNull(conversionService.convert(null, Integer.class));
} }
@Test(expected = IllegalArgumentException.class)
public void convertNullTargetClass() { public void convertNullTargetClass() {
assertEquals("3", conversionService.convert("3", (Class<?>) null)); assertNull(conversionService.convert("3", (Class<?>) null));
} }
@Test @Test
public void convertNullTypeDescriptor() { public void convertNullTypeDescriptor() {
assertEquals(null, conversionService.convert(3, TypeDescriptor.valueOf(String.class), TypeDescriptor.NULL)); assertNull(conversionService.convert("3", TypeDescriptor.valueOf(String.class), TypeDescriptor.NULL));
} }
@Test @Test
@ -132,6 +131,7 @@ public class GenericConversionServiceTests {
@Test @Test
public void convertArrayToArray() { public void convertArrayToArray() {
conversionService.addGenericConverter(Object[].class, Object[].class, new ArrayToArrayConverter(conversionService));
conversionService.addConverterFactory(new StringToNumberConverterFactory()); conversionService.addConverterFactory(new StringToNumberConverterFactory());
Integer[] result = conversionService.convert(new String[] { "1", "2", "3" }, Integer[].class); Integer[] result = conversionService.convert(new String[] { "1", "2", "3" }, Integer[].class);
assertEquals(new Integer(1), result[0]); assertEquals(new Integer(1), result[0]);

View File

@ -28,8 +28,7 @@ import org.springframework.expression.spel.SpelMessage;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* Default implementation of the {@link TypeConverter} interface, * Default implementation of the {@link TypeConverter} interface, delegating to a core Spring {@link ConversionService}.
* delegating to a core Spring {@link ConversionService}.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Andy Clement * @author Andy Clement
@ -56,12 +55,12 @@ public class StandardTypeConverter implements TypeConverter {
public Object convertValue(Object value, TypeDescriptor typeDescriptor) throws EvaluationException { public Object convertValue(Object value, TypeDescriptor typeDescriptor) throws EvaluationException {
try { try {
return this.conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor); return this.conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
} } catch (ConverterNotFoundException cenfe) {
catch (ConverterNotFoundException cenfe) { throw new SpelEvaluationException(cenfe, SpelMessage.TYPE_CONVERSION_ERROR, value != null ? value
throw new SpelEvaluationException(cenfe, SpelMessage.TYPE_CONVERSION_ERROR, value.getClass(), typeDescriptor.asString()); .getClass() : null, typeDescriptor.asString());
} } catch (ConversionException ce) {
catch (ConversionException ce) { throw new SpelEvaluationException(ce, SpelMessage.TYPE_CONVERSION_ERROR, value != null ? value.getClass()
throw new SpelEvaluationException(ce, SpelMessage.TYPE_CONVERSION_ERROR, value.getClass(), typeDescriptor.asString()); : null, typeDescriptor.asString());
} }
} }