Browse Source

Extend conditional conversion support

Introduce new ConditionalConversion interface that can be applied to
Converter, ConverterFactory or GenericConverter interfaces to make
them conditional. Prior to this commit the only
ConditionalGenericConverter could be conditional.

Issue: SPR-9928
pull/167/merge
Phillip Webb 12 years ago committed by Chris Beams
parent
commit
f13e3ad72b
  1. 55
      spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConversion.java
  2. 35
      spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java
  3. 7
      spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java
  4. 5
      spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java
  5. 12
      spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java
  6. 47
      spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java
  7. 134
      spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java

55
spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConversion.java

@ -0,0 +1,55 @@ @@ -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}.
*
* <p>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}.
*
* <p>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);
}

35
spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java

@ -1,5 +1,5 @@ @@ -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; @@ -18,34 +18,19 @@ package org.springframework.core.convert.converter;
import org.springframework.core.convert.TypeDescriptor;
/**
* A generic converter that conditionally executes.
*
* <p>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.
*
* <p>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</code> if the target field has also been annotated with <code>@DateTimeFormat</code>.
*
* <p>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
* <code>public static findAccount(String)</code> 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 <code>sourceType</code> to <code>targetType</code>
* 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 {
}

7
spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java

@ -1,5 +1,5 @@ @@ -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; @@ -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.
*
* <p>Implementations may additionally implement {@link ConditionalConversion}.
*
* @author Keith Donald
* @since 3.0
* @see ConditionalConversion
* @param <S> The source type
* @param <T> The target type
* @since 3.0
*/
public interface Converter<S, T> {

5
spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java

@ -1,5 +1,5 @@ @@ -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; @@ -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.
*
* <p>Implementations may additionally implement {@link ConditionalConversion}.
*
* @author Keith Donald
* @since 3.0
* @see ConditionalConversion
* @param <S> The source type converters created by this factory can convert from
* @param <R> The target range (or base) type converters created by this factory can convert to;
* for example {@link Number} for a set of number subtypes.

12
spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java

@ -1,5 +1,5 @@ @@ -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; @@ -34,18 +34,24 @@ import java.util.Set;
* <p>This interface should generally not be used when the simpler {@link Converter} or
* {@link ConverterFactory} interfaces are sufficient.
*
* <p>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.
* <p>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.
* <p>
* For {@link ConditionalConversion conditional} converters this method may return
* {@code null} to indicate all source-to-target pairs should be considered. *
*/
Set<ConvertiblePair> getConvertibleTypes();

47
spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java

@ -34,6 +34,7 @@ import org.springframework.core.convert.ConversionFailedException; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -393,15 +413,23 @@ public class GenericConversionService implements ConfigurableConversionService {
IGNORED_CLASSES = Collections.unmodifiableSet(ignored);
}
private final Set<GenericConverter> globalConverters =
new LinkedHashSet<GenericConverter>();
private final Map<ConvertiblePair, ConvertersForPair> converters =
new LinkedHashMap<ConvertiblePair, ConvertersForPair>(36);
public void add(GenericConverter converter) {
Set<ConvertiblePair> 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 { @@ -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;
}

134
spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java

@ -31,7 +31,9 @@ import java.util.Arrays; @@ -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; @@ -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 { @@ -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<ConvertiblePair> 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<TypeDescriptor> 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<String, Color>,
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<TypeDescriptor> sourceTypes = new LinkedHashSet<TypeDescriptor>();
public Set<ConvertiblePair> 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<TypeDescriptor> getSourceTypes() {
return sourceTypes;
}
}
private static class MyConditionalConverterFactory implements
ConverterFactory<String, Color>, ConditionalConversion {
private MyConditionalConverter converter = new MyConditionalConverter();
private int matchAttempts = 0;
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
matchAttempts++;
return true;
}
@SuppressWarnings("unchecked")
public <T extends Color> Converter<String, T> getConverter(Class<T> targetType) {
return (Converter<String, T>) converter;
}
public int getMatchAttempts() {
return matchAttempts;
}
public int getNestedMatchAttempts() {
return converter.getMatchAttempts();
}
}
}

Loading…
Cancel
Save