diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConversion.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConversion.java new file mode 100644 index 0000000000..1196b26fc4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConversion.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert.converter; + +import org.springframework.core.convert.TypeDescriptor; + +/** + * Allows a {@link Converter}, {@link GenericConverter} or {@link ConverterFactory} to + * conditionally execute based on attributes of the {@code source} and {@code target} + * {@link TypeDescriptor}. + * + *

Often used to selectively match custom conversion logic based on the presence of a + * field or class-level characteristic, such as an annotation or method. For example, when + * converting from a String field to a Date field, an implementation might return + * + * {@code true} if the target field has also been annotated with {@code @DateTimeFormat}. + * + *

As another example, when converting from a String field to an {@code Account} field, an + * implementation might return {@code true} if the target Account class defines a + * {@code public static findAccount(String)} method. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.2 + * @see Converter + * @see GenericConverter + * @see ConverterFactory + * @see ConditionalGenericConverter + */ +public interface ConditionalConversion { + + /** + * Should the converter from {@code sourceType} to {@code targetType} currently under + * consideration be selected? + * + * @param sourceType the type descriptor of the field we are converting from + * @param targetType the type descriptor of the field we are converting to + * @return true if conversion should be performed, false otherwise + */ + boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java index fd52ca7d87..fc7ce7a1ab 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,34 +18,19 @@ package org.springframework.core.convert.converter; import org.springframework.core.convert.TypeDescriptor; + /** - * A generic converter that conditionally executes. - * - *

Applies a rule that determines if a converter between a set of - * {@link #getConvertibleTypes() convertible types} matches given a client request to - * convert between a source field of convertible type S and a target field of convertible type T. - * - *

Often used to selectively match custom conversion logic based on the presence of - * a field or class-level characteristic, such as an annotation or method. For example, - * when converting from a String field to a Date field, an implementation might return - * true if the target field has also been annotated with @DateTimeFormat. - * - *

As another example, when converting from a String field to an Account field, - * an implementation might return true if the target Account class defines a - * public static findAccount(String) method. + * A {@link GenericConverter} that may conditionally execute based on attributes of the + * {@code source} and {@code target} {@link TypeDescriptor}. See + * {@link ConditionalConversion} for details. * * @author Keith Donald + * @author Phillip Webb * @since 3.0 + * @see GenericConverter + * @see ConditionalConversion */ -public interface ConditionalGenericConverter extends GenericConverter { - - /** - * Should the converter from sourceType to targetType - * currently under consideration be selected? - * @param sourceType the type descriptor of the field we are converting from - * @param targetType the type descriptor of the field we are converting to - * @return true if conversion should be performed, false otherwise - */ - boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); +public interface ConditionalGenericConverter extends GenericConverter, + ConditionalConversion { } diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java index 8417f581bc..a7b1a68d03 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,13 @@ package org.springframework.core.convert.converter; * A converter converts a source object of type S to a target of type T. * Implementations of this interface are thread-safe and can be shared. * + *

Implementations may additionally implement {@link ConditionalConversion}. + * * @author Keith Donald + * @since 3.0 + * @see ConditionalConversion * @param The source type * @param The target type - * @since 3.0 */ public interface Converter { diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java index fd4e5909dc..bece4d3266 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,11 @@ package org.springframework.core.convert.converter; /** * A factory for "ranged" converters that can convert objects from S to subtypes of R. * + *

Implementations may additionally implement {@link ConditionalConversion}. + * * @author Keith Donald * @since 3.0 + * @see ConditionalConversion * @param The source type converters created by this factory can convert from * @param The target range (or base) type converters created by this factory can convert to; * for example {@link Number} for a set of number subtypes. diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java index 84c93c8b99..3bbb1149f6 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,18 +34,24 @@ import java.util.Set; *

This interface should generally not be used when the simpler {@link Converter} or * {@link ConverterFactory} interfaces are sufficient. * + *

Implementations may additionally implement {@link ConditionalConversion}. + * * @author Keith Donald * @author Juergen Hoeller * @since 3.0 * @see TypeDescriptor * @see Converter * @see ConverterFactory + * @see ConditionalConversion */ public interface GenericConverter { /** - * Return the source and target types which this converter can convert between. - *

Each entry is a convertible source-to-target type pair. + * Return the source and target types which this converter can convert between. Each + * entry is a convertible source-to-target type pair. + *

+ * For {@link ConditionalConversion conditional} converters this method may return + * {@code null} to indicate all source-to-target pairs should be considered. * */ Set getConvertibleTypes(); diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index fa9383e2f2..205c66363e 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -34,6 +34,7 @@ import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConversion; import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; @@ -289,6 +290,10 @@ public class GenericConversionService implements ConfigurableConversionService { if(!this.typeInfo.getTargetType().equals(targetType.getObjectType())) { return false; } + if (this.converter instanceof ConditionalConversion) { + return ((ConditionalConversion) this.converter).matches(sourceType, + targetType); + } return true; } @@ -310,7 +315,7 @@ public class GenericConversionService implements ConfigurableConversionService { * Adapts a {@link ConverterFactory} to a {@link GenericConverter}. */ @SuppressWarnings("unchecked") - private final class ConverterFactoryAdapter implements GenericConverter { + private final class ConverterFactoryAdapter implements ConditionalGenericConverter { private final ConvertiblePair typeInfo; @@ -327,6 +332,21 @@ public class GenericConversionService implements ConfigurableConversionService { return Collections.singleton(this.typeInfo); } + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + boolean matches = true; + if (this.converterFactory instanceof ConditionalConversion) { + matches = ((ConditionalConversion) this.converterFactory).matches( + sourceType, targetType); + } + if(matches) { + Converter converter = converterFactory.getConverter(targetType.getType()); + if(converter instanceof ConditionalConversion) { + matches = ((ConditionalConversion) converter).matches(sourceType, targetType); + } + } + return matches; + } + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return convertNullSource(sourceType, targetType); @@ -393,15 +413,23 @@ public class GenericConversionService implements ConfigurableConversionService { IGNORED_CLASSES = Collections.unmodifiableSet(ignored); } + private final Set globalConverters = + new LinkedHashSet(); + private final Map converters = new LinkedHashMap(36); public void add(GenericConverter converter) { Set convertibleTypes = converter.getConvertibleTypes(); - Assert.state(converter.getConvertibleTypes() != null, "Converter does not specifiy ConvertibleTypes"); - for (ConvertiblePair convertiblePair : convertibleTypes) { - ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair); - convertersForPair.add(converter); + if (convertibleTypes == null) { + Assert.state(converter instanceof ConditionalConversion, + "Only conditional converters may return null convertible types"); + globalConverters.add(converter); + } else { + for (ConvertiblePair convertiblePair : convertibleTypes) { + ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair); + convertersForPair.add(converter); + } } } @@ -454,6 +482,15 @@ public class GenericConversionService implements ConfigurableConversionService { return converter; } + // Check ConditionalGenericConverter that match all types + for (GenericConverter globalConverter : this.globalConverters) { + if (((ConditionalConversion)globalConverter).matches( + sourceCandidate, + targetCandidate)) { + return globalConverter; + } + } + return null; } diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java index 9c87aed2dd..7d72d957b9 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -31,7 +31,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -42,7 +44,9 @@ import org.junit.Test; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConversion; import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.DescriptiveResource; import org.springframework.core.io.Resource; @@ -644,4 +648,134 @@ public class GenericConversionServiceTests { conversionService.removeConvertible(String.class, Color.class); assertFalse(conversionService.canConvert(String.class, Color.class)); } + + @Test + public void conditionalConverter() throws Exception { + GenericConversionService conversionService = new GenericConversionService(); + MyConditionalConverter converter = new MyConditionalConverter(); + conversionService.addConverter(new ColorConverter()); + conversionService.addConverter(converter); + assertEquals(Color.BLACK, conversionService.convert("#000000", Color.class)); + assertTrue(converter.getMatchAttempts() > 0); + } + + @Test + public void conditionalConverterFactory() throws Exception { + GenericConversionService conversionService = new GenericConversionService(); + MyConditionalConverterFactory converter = new MyConditionalConverterFactory(); + conversionService.addConverter(new ColorConverter()); + conversionService.addConverterFactory(converter); + assertEquals(Color.BLACK, conversionService.convert("#000000", Color.class)); + assertTrue(converter.getMatchAttempts() > 0); + assertTrue(converter.getNestedMatchAttempts() > 0); + } + + @Test + public void shouldNotSuportNullConvertibleTypesFromNonConditionalGenericConverter() + throws Exception { + GenericConversionService conversionService = new GenericConversionService(); + GenericConverter converter = new GenericConverter() { + + public Set getConvertibleTypes() { + return null; + } + + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return null; + } + }; + try { + conversionService.addConverter(converter); + fail("Did not throw"); + } catch (IllegalStateException e) { + assertEquals("Only conditional converters may return null convertible types", e.getMessage()); + } + } + + @Test + public void conditionalConversionForAllTypes() throws Exception { + GenericConversionService conversionService = new GenericConversionService(); + MyConditionalGenericConverter converter = new MyConditionalGenericConverter(); + conversionService.addConverter(converter); + assertEquals((Integer) 3, conversionService.convert(3, Integer.class)); + Iterator iterator = converter.getSourceTypes().iterator(); + assertEquals(Integer.class, iterator.next().getType()); + assertEquals(Number.class, iterator.next().getType()); + TypeDescriptor last = null; + while (iterator.hasNext()) { + last = iterator.next(); + } + assertEquals(Object.class, last.getType()); + } + + private static class MyConditionalConverter implements Converter, + ConditionalConversion { + + private int matchAttempts = 0; + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + matchAttempts++; + return false; + } + + public Color convert(String source) { + throw new IllegalStateException(); + } + + public int getMatchAttempts() { + return matchAttempts; + } + } + + private static class MyConditionalGenericConverter implements GenericConverter, + ConditionalConversion { + + private Set sourceTypes = new LinkedHashSet(); + + public Set getConvertibleTypes() { + return null; + } + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + sourceTypes.add(sourceType); + return false; + } + + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return null; + } + + public Set getSourceTypes() { + return sourceTypes; + } + } + + private static class MyConditionalConverterFactory implements + ConverterFactory, ConditionalConversion { + + private MyConditionalConverter converter = new MyConditionalConverter(); + + private int matchAttempts = 0; + + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + matchAttempts++; + return true; + } + + @SuppressWarnings("unchecked") + public Converter getConverter(Class targetType) { + return (Converter) converter; + } + + public int getMatchAttempts() { + return matchAttempts; + } + + public int getNestedMatchAttempts() { + return converter.getMatchAttempts(); + } + } + }