diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java index 4a0f6c23..d24bd751 100644 --- a/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java +++ b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java @@ -15,6 +15,8 @@ */ package org.springframework.binding.convert; +import java.util.Set; + /** * A service interface for retrieving type conversion executors. The returned command objects are thread-safe and may be * safely cached for use by client code. @@ -35,8 +37,8 @@ public interface ConversionService { public Object executeConversion(Object source, Class targetClass) throws ConversionException; /** - * Return the conversion executor capable of converting source objects of the specified sourceClass - * to instances of the targetClass. + * Return the default conversion executor capable of converting source objects of the specified + * sourceClass to instances of the targetClass. *

* The returned ConversionExecutor is thread-safe and may safely be cached for use in client code. * @param sourceClass the source class to convert from (required) @@ -47,17 +49,31 @@ public interface ConversionService { public ConversionExecutor getConversionExecutor(Class sourceClass, Class targetClass) throws ConversionExecutorNotFoundException; + /** + * Return the custom conversion executor capable of converting source objects of the specified + * sourceClass to instances of the targetClass. + *

+ * The returned ConversionExecutor is thread-safe and may safely be cached for use in client code. + * @param id the id of the custom conversion executor (required) + * @param sourceClass the source class to convert from (required) + * @param targetClass the target class to convert to (required) + * @return the executor that can execute instance type conversion, never null + * @throws ConversionExecutorNotFoundException when no suitable conversion executor could be found + */ + public ConversionExecutor getConversionExecutor(String id, Class sourceClass, Class targetClass) + throws ConversionExecutorNotFoundException; + /** * Return all conversion executors capable of converting from the provided sourceClass. For - * example, ConversionExecutor[] getConversionExecutor(String.class) would return all converters that - * convert from String to some other Object. + * example, getConversionExecutor(String.class) would return all converters that convert from String + * to some other Object. Mainly useful for adapting a set of converters to some other environment. * @param sourceClass the source class converting from * @return the conversion executors that can convert from that source class */ - public ConversionExecutor[] getConversionExecutors(Class sourceClass); + public Set getConversionExecutors(Class sourceClass); /** - * Lookup a class by it alias. For example, long for java.lang.Long + * Lookup a class by its well-known alias. For example, long for java.lang.Long * @param alias the class alias * @return the class, or null if no alias exists */ diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/converters/FormattedStringToNumber.java b/spring-binding/src/main/java/org/springframework/binding/convert/converters/FormattedStringToNumber.java index 379aff32..0a7073f2 100644 --- a/spring-binding/src/main/java/org/springframework/binding/convert/converters/FormattedStringToNumber.java +++ b/spring-binding/src/main/java/org/springframework/binding/convert/converters/FormattedStringToNumber.java @@ -50,6 +50,10 @@ public class FormattedStringToNumber extends StringToObject { private boolean lenient; + public FormattedStringToNumber() { + super(Number.class); + } + public FormattedStringToNumber(Class numberClass) { super(numberClass); } diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/service/GenericConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/service/GenericConversionService.java index bff36789..bc50561d 100644 --- a/spring-binding/src/main/java/org/springframework/binding/convert/service/GenericConversionService.java +++ b/spring-binding/src/main/java/org/springframework/binding/convert/service/GenericConversionService.java @@ -16,13 +16,12 @@ package org.springframework.binding.convert.service; import java.lang.reflect.Modifier; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Set; @@ -52,6 +51,12 @@ public class GenericConversionService implements ConversionService { */ private final Map sourceClassConverters = new HashMap(); + /** + * A map of custom converters. Custom converters are assigned a unique identifier that can be used to lookup the + * converter. This allows multiple converters for the same source->target class to be registered. + */ + private final Map customConverters = new HashMap(); + /** * Indexes classes by well-known aliases. */ @@ -83,14 +88,6 @@ public class GenericConversionService implements ConversionService { public void addConverter(Converter converter) { Class sourceClass = converter.getSourceClass(); Class targetClass = converter.getTargetClass(); - if (sourceClass.isPrimitive()) { - throw new IllegalArgumentException("Invalid Converter " + converter - + "; A primitive type is not allowed for the [sourceClass] argument"); - } - if (targetClass.isPrimitive()) { - throw new IllegalArgumentException("Invalid Converter " + converter - + "; A primitive type is not allowed for the [targetClass] argument"); - } Map sourceMap = getSourceMap(sourceClass); sourceMap.put(targetClass, converter); if (converter instanceof TwoWayConverter) { @@ -99,6 +96,15 @@ public class GenericConversionService implements ConversionService { } } + /** + * Add given custom converter to this conversion service. + * @param id the id of the custom converter instance + * @param converter the converter + */ + public void addConverter(String id, Converter converter) { + customConverters.put(id, converter); + } + /** * Add an alias for given target type. */ @@ -154,12 +160,42 @@ public class GenericConversionService implements ConversionService { return parent.getConversionExecutor(sourceClass, targetClass); } else { throw new ConversionExecutorNotFoundException(sourceClass, targetClass, - "No ConversionExecutor found for converting from sourceClass '" + sourceClass.getName() - + "' to target class '" + targetClass.getName() + "'"); + "No ConversionExecutor found for converting from sourceClass [" + sourceClass.getName() + + "] to target class [" + targetClass.getName() + "]"); } } } + public ConversionExecutor getConversionExecutor(String id, Class sourceClass, Class targetClass) + throws ConversionExecutorNotFoundException { + Converter converter = (Converter) customConverters.get(id); + if (converter == null) { + throw new ConversionExecutorNotFoundException(sourceClass, targetClass, + "No custom ConversionExecutor found with id '" + id + "' for converting from sourceClass [" + + sourceClass.getName() + "] to targetClass [" + targetClass.getName() + "]"); + } + if (converter.getSourceClass().isAssignableFrom(sourceClass)) { + if (!converter.getTargetClass().isAssignableFrom(targetClass)) { + throw new ConversionExecutorNotFoundException(sourceClass, targetClass, + "Custom ConversionExecutor with id '" + id + "' cannot convert from sourceClass [" + + sourceClass.getName() + "] to targetClass [" + targetClass.getName() + "]"); + } + return new StaticConversionExecutor(sourceClass, targetClass, converter); + } else if (converter.getTargetClass().isAssignableFrom(sourceClass) && converter instanceof TwoWayConverter) { + if (!converter.getSourceClass().isAssignableFrom(targetClass)) { + throw new ConversionExecutorNotFoundException(sourceClass, targetClass, + "Custom ConversionExecutor with id '" + id + "' cannot convert from sourceClass [" + + sourceClass.getName() + "] to targetClass [" + targetClass.getName() + "]"); + } + TwoWayConverter twoWay = (TwoWayConverter) converter; + return new StaticConversionExecutor(sourceClass, targetClass, new ReverseConverter(twoWay)); + } else { + throw new ConversionExecutorNotFoundException(sourceClass, targetClass, + "Custom ConversionExecutor with id '" + id + "' cannot convert from sourceClass [" + + sourceClass.getName() + "] to targetClass [" + targetClass.getName() + "]"); + } + } + private Converter findRegisteredConverter(Class sourceClass, Class targetClass) { if (sourceClass.isInterface()) { LinkedList classQueue = new LinkedList(); @@ -224,20 +260,27 @@ public class GenericConversionService implements ConversionService { // subclassing support - public ConversionExecutor[] getConversionExecutors(Class sourceClass) { + public Set getConversionExecutors(Class sourceClass) { + Set parentExecutors; + if (parent != null) { + parentExecutors = parent.getConversionExecutors(sourceClass); + } else { + parentExecutors = Collections.EMPTY_SET; + } Map sourceMap = getSourceMap(sourceClass); - if (sourceMap.isEmpty()) { - return new ConversionExecutor[0]; + if (parentExecutors.isEmpty() && sourceMap.isEmpty()) { + return Collections.EMPTY_SET; } Set entries = sourceMap.entrySet(); - List conversionExecutors = new ArrayList(entries.size()); + Set conversionExecutors = new HashSet(entries.size() + parentExecutors.size()); for (Iterator it = entries.iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); Class targetClass = (Class) entry.getKey(); Converter converter = (Converter) entry.getValue(); conversionExecutors.add(new StaticConversionExecutor(sourceClass, targetClass, converter)); } - return (ConversionExecutor[]) conversionExecutors.toArray(new ConversionExecutor[entries.size()]); + conversionExecutors.addAll(parentExecutors); + return conversionExecutors; } /** diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/beanwrapper/BeanWrapperExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/beanwrapper/BeanWrapperExpression.java index 049c9345..0888c269 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/beanwrapper/BeanWrapperExpression.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/beanwrapper/BeanWrapperExpression.java @@ -16,6 +16,8 @@ package org.springframework.binding.expression.beanwrapper; import java.beans.PropertyEditorSupport; +import java.util.Iterator; +import java.util.Set; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeansException; @@ -88,9 +90,9 @@ public class BeanWrapperExpression implements Expression { public void setValue(Object context, Object value) { try { BeanWrapperImpl beanWrapper = new BeanWrapperImpl(context); - ConversionExecutor[] converters = conversionService.getConversionExecutors(String.class); - for (int i = 0; i < converters.length; i++) { - ConversionExecutor converter = converters[i]; + Set converters = conversionService.getConversionExecutors(String.class); + for (Iterator it = converters.iterator(); it.hasNext();) { + ConversionExecutor converter = (ConversionExecutor) it.next(); beanWrapper.registerCustomEditor(converter.getTargetClass(), new PropertyEditorConverter(converter)); } beanWrapper.setPropertyValue(expression, value); diff --git a/spring-binding/src/test/java/org/springframework/binding/convert/service/DefaultConversionServiceTests.java b/spring-binding/src/test/java/org/springframework/binding/convert/service/DefaultConversionServiceTests.java index 55fac979..426678dd 100644 --- a/spring-binding/src/test/java/org/springframework/binding/convert/service/DefaultConversionServiceTests.java +++ b/spring-binding/src/test/java/org/springframework/binding/convert/service/DefaultConversionServiceTests.java @@ -15,13 +15,17 @@ */ package org.springframework.binding.convert.service; +import java.math.BigDecimal; +import java.security.Principal; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Set; import junit.framework.TestCase; @@ -39,6 +43,7 @@ import org.springframework.binding.format.DefaultNumberFormatFactory; * @author Keith Donald */ public class DefaultConversionServiceTests extends TestCase { + public void testConvertCompatibleTypes() { DefaultConversionService service = new DefaultConversionService(); List lst = new ArrayList(); @@ -85,7 +90,7 @@ public class DefaultConversionServiceTests extends TestCase { public void testRegisterConverter() { GenericConversionService service = new GenericConversionService(); - FormattedStringToNumber converter = new FormattedStringToNumber(Integer.class); + FormattedStringToNumber converter = new FormattedStringToNumber(); DefaultNumberFormatFactory numberFormatFactory = new DefaultNumberFormatFactory(); numberFormatFactory.setLocale(Locale.US); converter.setNumberFormatFactory(numberFormatFactory); @@ -98,6 +103,21 @@ public class DefaultConversionServiceTests extends TestCase { assertEquals("3,000", string); } + public void testRegisterCustomConverter() { + DefaultConversionService service = new DefaultConversionService(); + FormattedStringToNumber converter = new FormattedStringToNumber(); + DefaultNumberFormatFactory numberFormatFactory = new DefaultNumberFormatFactory(); + numberFormatFactory.setLocale(Locale.US); + converter.setNumberFormatFactory(numberFormatFactory); + service.addConverter("usaNumber", converter); + ConversionExecutor executor = service.getConversionExecutor("usaNumber", String.class, Integer.class); + Integer three = (Integer) executor.execute("3,000"); + assertEquals(3000, three.intValue()); + ConversionExecutor executor2 = service.getConversionExecutor("usaNumber", Integer.class, String.class); + String string = (String) executor2.execute(new Integer(3000)); + assertEquals("3,000", string); + } + public void testConversionPrimitive() { DefaultConversionService service = new DefaultConversionService(); ConversionExecutor executor = service.getConversionExecutor(String.class, int.class); @@ -200,6 +220,42 @@ public class DefaultConversionServiceTests extends TestCase { assertEquals(new Integer(123), result[0]); } + public void testGetConversionExecutorsForSource() { + DefaultConversionService service1 = new DefaultConversionService(); + service1.addConverter(new CustomConverter()); + GenericConversionService service2 = new GenericConversionService(); + FormattedStringToNumber formatterConverter = new FormattedStringToNumber(BigDecimal.class); + service2.addConverter(formatterConverter); + service2.setParent(service1); + Set converters = service2.getConversionExecutors(String.class); + Iterator it = converters.iterator(); + while (it.hasNext()) { + ConversionExecutor executor = (ConversionExecutor) it.next(); + if (executor.getTargetClass().equals(BigDecimal.class)) { + StaticConversionExecutor se = (StaticConversionExecutor) executor; + assertSame(formatterConverter, se.getConverter()); + } + } + assertEquals(14, converters.size()); + } + + private static class CustomConverter implements Converter { + + public Object convertSourceToTargetClass(Object source, Class targetClass) throws Exception { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Auto-generated method stub"); + } + + public Class getSourceClass() { + return String.class; + } + + public Class getTargetClass() { + return Principal.class; + } + + } + // public void testGenericTypeConversionOGNL() { // DefaultConversionService service = new DefaultConversionService(); // Map map = new HashMap();