Commit 37a66349 authored by Andy Wilkinson's avatar Andy Wilkinson

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
parent 30f79f2f
...@@ -18,12 +18,16 @@ package org.springframework.boot.context.properties.bind; ...@@ -18,12 +18,16 @@ package org.springframework.boot.context.properties.bind;
import java.beans.PropertyEditor; import java.beans.PropertyEditor;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.PropertyEditorRegistry;
...@@ -42,6 +46,7 @@ import org.springframework.util.Assert; ...@@ -42,6 +46,7 @@ import org.springframework.util.Assert;
* and so a new instance is created for each top-level bind. * and so a new instance is created for each top-level bind.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
*/ */
class BindConverter { class BindConverter {
...@@ -52,24 +57,21 @@ class BindConverter { ...@@ -52,24 +57,21 @@ class BindConverter {
EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded); EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded);
} }
private final ConversionService typeConverterConversionService;
private final ConversionService conversionService; private final ConversionService conversionService;
BindConverter(ConversionService conversionService, BindConverter(ConversionService conversionService,
Consumer<PropertyEditorRegistry> propertyEditorInitializer) { Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
Assert.notNull(conversionService, "ConversionService must not be null"); Assert.notNull(conversionService, "ConversionService must not be null");
this.typeConverterConversionService = new TypeConverterConversionService( this.conversionService = new CompositeConversionService(
propertyEditorInitializer); new TypeConverterConversionService(propertyEditorInitializer),
this.conversionService = conversionService; conversionService);
} }
public boolean canConvert(Object value, ResolvableType type, public boolean canConvert(Object value, ResolvableType type,
Annotation... annotations) { Annotation... annotations) {
TypeDescriptor sourceType = TypeDescriptor.forObject(value); TypeDescriptor sourceType = TypeDescriptor.forObject(value);
TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations); TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations);
return this.typeConverterConversionService.canConvert(sourceType, targetType) return this.conversionService.canConvert(sourceType, targetType);
|| this.conversionService.canConvert(sourceType, targetType);
} }
public <T> T convert(Object result, Bindable<T> target) { public <T> T convert(Object result, Bindable<T> target) {
...@@ -83,10 +85,6 @@ class BindConverter { ...@@ -83,10 +85,6 @@ class BindConverter {
} }
TypeDescriptor sourceType = TypeDescriptor.forObject(value); TypeDescriptor sourceType = TypeDescriptor.forObject(value);
TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations); 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); return (T) this.conversionService.convert(value, sourceType, targetType);
} }
...@@ -180,4 +178,66 @@ class BindConverter { ...@@ -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; ...@@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.bind;
import java.beans.PropertyEditorSupport; import java.beans.PropertyEditorSupport;
import java.io.File; import java.io.File;
import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
...@@ -45,6 +46,7 @@ import static org.mockito.Mockito.verify; ...@@ -45,6 +46,7 @@ import static org.mockito.Mockito.verify;
* Tests for {@link BindConverter}. * Tests for {@link BindConverter}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
*/ */
public class BindConverterTests { public class BindConverterTests {
...@@ -202,10 +204,17 @@ public class BindConverterTests { ...@@ -202,10 +204,17 @@ public class BindConverterTests {
// classpath resource reference. See gh-12163 // classpath resource reference. See gh-12163
BindConverter bindConverter = new BindConverter(new GenericConversionService(), BindConverter bindConverter = new BindConverter(new GenericConversionService(),
null); null);
assertThat(bindConverter.canConvert(".", ResolvableType.forClass(File.class))) File result = bindConverter.convert(".", ResolvableType.forClass(File.class));
.isFalse(); assertThat(result.getPath()).isEqualTo(".");
this.thrown.expect(ConverterNotFoundException.class); }
bindConverter.convert(".", ResolvableType.forClass(File.class));
@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( private BindConverter getPropertyEditorOnlyBindConverter(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment