Fall back to application conversion service in BindConverter

Previously, if a user declared a custom conversionService bean that
was not an ApplicationConversionService instance, the binder lost
the ability to convert a String to a Duration (along with any other
conversions that are specific to ApplicationConversionService).

This commit updates BindConverter so that, if the ConversionService
with which it is created is not an ApplicationConversionService, it
will use one as an additional service when performing conversion.

Closes gh-12237
pull/12272/head
Andy Wilkinson 7 years ago
parent 30f79f2fb1
commit 37a66349fe

@ -18,12 +18,16 @@ package org.springframework.boot.context.properties.bind;
import java.beans.PropertyEditor;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyEditorRegistry;
@ -42,6 +46,7 @@ import org.springframework.util.Assert;
* and so a new instance is created for each top-level bind.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class BindConverter {
@ -52,24 +57,21 @@ class BindConverter {
EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded);
}
private final ConversionService typeConverterConversionService;
private final ConversionService conversionService;
BindConverter(ConversionService conversionService,
Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
Assert.notNull(conversionService, "ConversionService must not be null");
this.typeConverterConversionService = new TypeConverterConversionService(
propertyEditorInitializer);
this.conversionService = conversionService;
this.conversionService = new CompositeConversionService(
new TypeConverterConversionService(propertyEditorInitializer),
conversionService);
}
public boolean canConvert(Object value, ResolvableType type,
Annotation... annotations) {
TypeDescriptor sourceType = TypeDescriptor.forObject(value);
TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations);
return this.typeConverterConversionService.canConvert(sourceType, targetType)
|| this.conversionService.canConvert(sourceType, targetType);
return this.conversionService.canConvert(sourceType, targetType);
}
public <T> T convert(Object result, Bindable<T> target) {
@ -83,10 +85,6 @@ class BindConverter {
}
TypeDescriptor sourceType = TypeDescriptor.forObject(value);
TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations);
if (this.typeConverterConversionService.canConvert(sourceType, targetType)) {
return (T) this.typeConverterConversionService.convert(value, sourceType,
targetType);
}
return (T) this.conversionService.convert(value, sourceType, targetType);
}
@ -180,4 +178,66 @@ class BindConverter {
}
private static final class CompositeConversionService implements ConversionService {
private final List<ConversionService> delegates;
private CompositeConversionService(
TypeConverterConversionService typeConverterConversionService,
ConversionService conversionService) {
List<ConversionService> delegates = new ArrayList<ConversionService>();
delegates.add(typeConverterConversionService);
delegates.add(conversionService);
if (!(conversionService instanceof ApplicationConversionService)) {
delegates.add(ApplicationConversionService.getSharedInstance());
}
this.delegates = delegates;
}
@Override
public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
return canConvert((delegate) -> delegate.canConvert(sourceType, targetType));
}
@Override
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
return canConvert((delegate) -> delegate.canConvert(sourceType, targetType));
}
private boolean canConvert(Predicate<ConversionService> canConvert) {
for (ConversionService delegate : this.delegates) {
if (canConvert.test(delegate)) {
return true;
}
}
return false;
}
@Override
public <T> T convert(Object source, Class<T> targetType) {
Class<?> sourceType = source.getClass();
return convert((delegate) -> delegate.canConvert(sourceType, targetType),
(delegate) -> delegate.convert(source, targetType));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
return convert((delegate) -> delegate.canConvert(sourceType, targetType),
(delegate) -> delegate.convert(source, sourceType, targetType));
}
public <T> T convert(Predicate<ConversionService> canConvert,
Function<ConversionService, T> convert) {
for (int i = 0; i < this.delegates.size() - 1; i++) {
ConversionService delegate = this.delegates.get(i);
if (canConvert.test(delegate)) {
return convert.apply(delegate);
}
}
return convert.apply(this.delegates.get(this.delegates.size() - 1));
}
}
}

@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.bind;
import java.beans.PropertyEditorSupport;
import java.io.File;
import java.time.Duration;
import java.util.List;
import java.util.function.Consumer;
@ -45,6 +46,7 @@ import static org.mockito.Mockito.verify;
* Tests for {@link BindConverter}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class BindConverterTests {
@ -202,10 +204,17 @@ public class BindConverterTests {
// classpath resource reference. See gh-12163
BindConverter bindConverter = new BindConverter(new GenericConversionService(),
null);
assertThat(bindConverter.canConvert(".", ResolvableType.forClass(File.class)))
.isFalse();
this.thrown.expect(ConverterNotFoundException.class);
bindConverter.convert(".", ResolvableType.forClass(File.class));
File result = bindConverter.convert(".", ResolvableType.forClass(File.class));
assertThat(result.getPath()).isEqualTo(".");
}
@Test
public void fallsBackToApplicationConversionService() {
BindConverter bindConverter = new BindConverter(new GenericConversionService(),
null);
Duration result = bindConverter.convert("10s",
ResolvableType.forClass(Duration.class));
assertThat(result.getSeconds()).isEqualTo(10);
}
private BindConverter getPropertyEditorOnlyBindConverter(

Loading…
Cancel
Save