Fix bind TypeConverter delegation with collections
Update `BindConverter` so that delegation to `SimpleTypeConverter` also works with Collections and Arrays. Prior to this commit, conversion that relied on a `PropertyEditor` would only work for simple types. For example, "String -> Class<?>" would use the `ClassEditor` but "String -> List<Class<?>>" would fail. The `BindConverter` now uses a minimal `ConversionService` as an adapter to the `SimpleTypeConverter`. This allows us to use the same delimited string conversion logic as the `ApplicationConverter`. Fixes gh-12166
This commit is contained in:
parent
4b9c3c137e
commit
3dea6fc645
|
@ -19,15 +19,20 @@ package org.springframework.boot.context.properties.bind;
|
|||
import java.beans.PropertyEditor;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.PropertyEditorRegistry;
|
||||
import org.springframework.beans.SimpleTypeConverter;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.ConditionalGenericConverter;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
|
@ -38,25 +43,24 @@ import org.springframework.util.Assert;
|
|||
*/
|
||||
class BindConverter {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
private final ConversionService typeConverterConversionService;
|
||||
|
||||
private final SimpleTypeConverter simpleTypeConverter;
|
||||
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.simpleTypeConverter = new SimpleTypeConverter();
|
||||
if (propertyEditorInitializer != null) {
|
||||
propertyEditorInitializer.accept(this.simpleTypeConverter);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canConvert(Object value, ResolvableType type,
|
||||
Annotation... annotations) {
|
||||
return getPropertyEditor(type.resolve()) != null
|
||||
|| this.conversionService.canConvert(TypeDescriptor.forObject(value),
|
||||
new ResolvableTypeDescriptor(type, annotations));
|
||||
TypeDescriptor sourceType = TypeDescriptor.forObject(value);
|
||||
TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations);
|
||||
return this.typeConverterConversionService.canConvert(sourceType, targetType)
|
||||
|| this.conversionService.canConvert(sourceType, targetType);
|
||||
}
|
||||
|
||||
public <T> T convert(Object result, Bindable<T> target) {
|
||||
|
@ -65,37 +69,22 @@ class BindConverter {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T convert(Object value, ResolvableType type, Annotation... annotations) {
|
||||
PropertyEditor propertyEditor = getPropertyEditor(type.resolve());
|
||||
if (propertyEditor != null) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return (T) this.simpleTypeConverter.convertIfNecessary(value, type.resolve());
|
||||
}
|
||||
return (T) this.conversionService.convert(value, TypeDescriptor.forObject(value),
|
||||
new ResolvableTypeDescriptor(type, annotations));
|
||||
}
|
||||
|
||||
private PropertyEditor getPropertyEditor(Class<?> type) {
|
||||
if (type == null || type == Object.class
|
||||
|| Collection.class.isAssignableFrom(type)
|
||||
|| Map.class.isAssignableFrom(type)) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
PropertyEditor editor = this.simpleTypeConverter.getDefaultEditor(type);
|
||||
if (editor == null) {
|
||||
editor = this.simpleTypeConverter.findCustomEditor(type, null);
|
||||
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);
|
||||
}
|
||||
if (editor == null && String.class != type) {
|
||||
editor = BeanUtils.findEditorByConvention(type);
|
||||
}
|
||||
return editor;
|
||||
return (T) this.conversionService.convert(value, sourceType, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link TypeDescriptor} backed by a {@link ResolvableType}.
|
||||
*/
|
||||
final class ResolvableTypeDescriptor extends TypeDescriptor {
|
||||
private static class ResolvableTypeDescriptor extends TypeDescriptor {
|
||||
|
||||
ResolvableTypeDescriptor(ResolvableType resolvableType,
|
||||
Annotation[] annotations) {
|
||||
|
@ -103,4 +92,80 @@ class BindConverter {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ConversionService} implementation that delegates to a
|
||||
* {@link SimpleTypeConverter}. Allows {@link PropertyEditor} based conversion for
|
||||
* simple types, arrays and collections.
|
||||
*/
|
||||
private static class TypeConverterConversionService extends GenericConversionService {
|
||||
|
||||
private SimpleTypeConverter typeConverter;
|
||||
|
||||
TypeConverterConversionService(Consumer<PropertyEditorRegistry> initializer) {
|
||||
this.typeConverter = new SimpleTypeConverter();
|
||||
if (initializer != null) {
|
||||
initializer.accept(this.typeConverter);
|
||||
}
|
||||
addConverter(new TypeConverterConverter(this.typeConverter));
|
||||
ApplicationConversionService.addDelimitedStringConverters(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
// Prefer conversion service to handle things like String to char[].
|
||||
if (targetType.isArray()
|
||||
&& targetType.getElementTypeDescriptor().isPrimitive()) {
|
||||
return false;
|
||||
}
|
||||
return super.canConvert(sourceType, targetType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ConditionalGenericConverter} that delegates to {@link SimpleTypeConverter}.
|
||||
*/
|
||||
private static class TypeConverterConverter implements ConditionalGenericConverter {
|
||||
|
||||
private SimpleTypeConverter typeConverter;
|
||||
|
||||
TypeConverterConverter(SimpleTypeConverter typeConverter) {
|
||||
this.typeConverter = typeConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Collections.singleton(new ConvertiblePair(String.class, Object.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
return getPropertyEditor(targetType.getType()) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType,
|
||||
TypeDescriptor targetType) {
|
||||
return this.typeConverter.convertIfNecessary(source, targetType.getType());
|
||||
}
|
||||
|
||||
private PropertyEditor getPropertyEditor(Class<?> type) {
|
||||
if (type == null || type == Object.class
|
||||
|| Collection.class.isAssignableFrom(type)
|
||||
|| Map.class.isAssignableFrom(type)) {
|
||||
return null;
|
||||
}
|
||||
PropertyEditor editor = this.typeConverter.getDefaultEditor(type);
|
||||
if (editor == null) {
|
||||
editor = this.typeConverter.findCustomEditor(type, null);
|
||||
}
|
||||
if (editor == null && String.class != type) {
|
||||
editor = BeanUtils.findEditorByConvention(type);
|
||||
}
|
||||
return editor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.convert;
|
|||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.ConfigurableConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
|
@ -48,10 +49,7 @@ public class ApplicationConversionService extends FormattingConversionService {
|
|||
if (embeddedValueResolver != null) {
|
||||
setEmbeddedValueResolver(embeddedValueResolver);
|
||||
}
|
||||
DefaultConversionService.addDefaultConverters(this);
|
||||
DefaultFormattingConversionService.addDefaultFormatters(this);
|
||||
addApplicationConverters(this);
|
||||
addApplicationFormatters(this);
|
||||
configure(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,12 +71,31 @@ public class ApplicationConversionService extends FormattingConversionService {
|
|||
return sharedInstance;
|
||||
}
|
||||
|
||||
public void addApplicationConverters(ConverterRegistry registry) {
|
||||
ConversionService service = (ConversionService) registry;
|
||||
registry.addConverter(new ArrayToDelimitedStringConverter(service));
|
||||
registry.addConverter(new CollectionToDelimitedStringConverter(service));
|
||||
registry.addConverter(new DelimitedStringToArrayConverter(service));
|
||||
registry.addConverter(new DelimitedStringToCollectionConverter(service));
|
||||
/**
|
||||
* Configure the given {@link FormatterRegistry} with formatters and converts
|
||||
* appropriate for most Spring Boot applications.
|
||||
* @param registry the registry of converters to add to (must also be castable to
|
||||
* ConversionService, e.g. being a {@link ConfigurableConversionService})
|
||||
* @throws ClassCastException if the given ConverterRegistry could not be cast to a
|
||||
* ConversionService
|
||||
*/
|
||||
public static void configure(FormatterRegistry registry) {
|
||||
DefaultConversionService.addDefaultConverters(registry);
|
||||
DefaultFormattingConversionService.addDefaultFormatters(registry);
|
||||
addApplicationFormatters(registry);
|
||||
addApplicationConverters(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add converters useful for most Spring Boot applications.
|
||||
* {@link DefaultConversionService#addDefaultConverters(ConverterRegistry)}
|
||||
* @param registry the registry of converters to add to (must also be castable to
|
||||
* ConversionService, e.g. being a {@link ConfigurableConversionService})
|
||||
* @throws ClassCastException if the given ConverterRegistry could not be cast to a
|
||||
* ConversionService
|
||||
*/
|
||||
public static void addApplicationConverters(ConverterRegistry registry) {
|
||||
addDelimitedStringConverters(registry);
|
||||
registry.addConverter(new StringToDurationConverter());
|
||||
registry.addConverter(new DurationToStringConverter());
|
||||
registry.addConverter(new NumberToDurationConverter());
|
||||
|
@ -86,7 +103,26 @@ public class ApplicationConversionService extends FormattingConversionService {
|
|||
registry.addConverterFactory(new StringToEnumIgnoringCaseConverterFactory());
|
||||
}
|
||||
|
||||
public void addApplicationFormatters(FormatterRegistry registry) {
|
||||
/**
|
||||
* Add converters to support delimited strings.
|
||||
* @param registry the registry of converters to add to (must also be castable to
|
||||
* ConversionService, e.g. being a {@link ConfigurableConversionService})
|
||||
* @throws ClassCastException if the given ConverterRegistry could not be cast to a
|
||||
* ConversionService
|
||||
*/
|
||||
public static void addDelimitedStringConverters(ConverterRegistry registry) {
|
||||
ConversionService service = (ConversionService) registry;
|
||||
registry.addConverter(new ArrayToDelimitedStringConverter(service));
|
||||
registry.addConverter(new CollectionToDelimitedStringConverter(service));
|
||||
registry.addConverter(new DelimitedStringToArrayConverter(service));
|
||||
registry.addConverter(new DelimitedStringToCollectionConverter(service));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add formatters useful for most Spring Boot applications.
|
||||
* @param registry the service to register default formatters with
|
||||
*/
|
||||
public static void addApplicationFormatters(FormatterRegistry registry) {
|
||||
registry.addFormatter(new CharArrayFormatter());
|
||||
registry.addFormatter(new InetAddressFormatter());
|
||||
registry.addFormatter(new IsoOffsetFormatter());
|
||||
|
|
|
@ -728,6 +728,15 @@ public class ConfigurationPropertiesTests {
|
|||
assertThat(bean.getPerson().lastName).isEqualTo("boot");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadWhenBindingToListOfGenericClassShouldBind() {
|
||||
// gh-12166
|
||||
load(ListOfGenericClassProperties.class, "test.list=java.lang.RuntimeException");
|
||||
ListOfGenericClassProperties bean = this.context
|
||||
.getBean(ListOfGenericClassProperties.class);
|
||||
assertThat(bean.getList()).containsExactly(RuntimeException.class);
|
||||
}
|
||||
|
||||
private AnnotationConfigApplicationContext load(Class<?> configuration,
|
||||
String... inlinedProperties) {
|
||||
return load(new Class<?>[] { configuration }, inlinedProperties);
|
||||
|
@ -1599,6 +1608,22 @@ public class ConfigurationPropertiesTests {
|
|||
|
||||
}
|
||||
|
||||
@EnableConfigurationProperties
|
||||
@ConfigurationProperties(prefix = "test")
|
||||
static class ListOfGenericClassProperties {
|
||||
|
||||
private List<Class<? extends Throwable>> list;
|
||||
|
||||
public List<Class<? extends Throwable>> getList() {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
public void setList(List<Class<? extends Throwable>> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomPropertiesValidator implements Validator {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
* Copyright 2012-2018 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.
|
||||
|
@ -278,4 +278,25 @@ public class ArrayBinderTests {
|
|||
assertThat(result).containsExactly("1", "2", "3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToArrayShouldUsePropertyEditor() {
|
||||
// gh-12166
|
||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||
source.put("foo[0]", "java.lang.RuntimeException");
|
||||
source.put("foo[1]", "java.lang.IllegalStateException");
|
||||
this.sources.add(source);
|
||||
assertThat(this.binder.bind("foo", Bindable.of(Class[].class)).get())
|
||||
.containsExactly(RuntimeException.class, IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToArrayWhenStringShouldUsePropertyEditor() {
|
||||
// gh-12166
|
||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||
source.put("foo", "java.lang.RuntimeException,java.lang.IllegalStateException");
|
||||
this.sources.add(source);
|
||||
assertThat(this.binder.bind("foo", Bindable.of(Class[].class)).get())
|
||||
.containsExactly(RuntimeException.class, IllegalStateException.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* Copyright 2012-2018 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.boot.context.properties.bind;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.beans.PropertyEditorRegistry;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.ConverterNotFoundException;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link BindConverter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class BindConverterTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Mock
|
||||
private Consumer<PropertyEditorRegistry> propertyEditorInitializer;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenConversionServiceIsNullShouldThrowException() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("ConversionService must not be null");
|
||||
new BindConverter(null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenPropertyEditorInitializerIsNullShouldCreate() {
|
||||
BindConverter bindConverter = new BindConverter(
|
||||
ApplicationConversionService.getSharedInstance(), null);
|
||||
assertThat(bindConverter).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenPropertyEditorInitializerIsNotNullShouldUseToInitialize() {
|
||||
new BindConverter(ApplicationConversionService.getSharedInstance(),
|
||||
this.propertyEditorInitializer);
|
||||
verify(this.propertyEditorInitializer).accept(any(PropertyEditorRegistry.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canConvertWhenHasDefaultEditorShouldReturnTrue() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(null);
|
||||
assertThat(bindConverter.canConvert("java.lang.RuntimeException",
|
||||
ResolvableType.forClass(Class.class))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canConvertWhenHasCustomEditorShouldReturnTrue() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(
|
||||
this::registerSampleTypeEditor);
|
||||
assertThat(bindConverter.canConvert("test",
|
||||
ResolvableType.forClass(SampleType.class))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canConvertWhenHasEditorByConventionShouldReturnTrue() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(null);
|
||||
assertThat(bindConverter.canConvert("test",
|
||||
ResolvableType.forClass(ConventionType.class))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canConvertWhenHasEditorForCollectionElementShouldReturnTrue() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(
|
||||
this::registerSampleTypeEditor);
|
||||
assertThat(bindConverter.canConvert("test",
|
||||
ResolvableType.forClassWithGenerics(List.class, SampleType.class)))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canConvertWhenHasEditorForArrayElementShouldReturnTrue() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(
|
||||
this::registerSampleTypeEditor);
|
||||
assertThat(bindConverter.canConvert("test",
|
||||
ResolvableType.forClass(SampleType[].class))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canConvertWhenConversionServiceCanConvertShouldReturnTrue() {
|
||||
BindConverter bindConverter = getBindConverter(new SampleTypeConverter());
|
||||
assertThat(bindConverter.canConvert("test",
|
||||
ResolvableType.forClass(SampleType.class))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canConvertWhenNotPropertyEditorAndConversionServiceCannotConvertShouldReturnFalse() {
|
||||
BindConverter bindConverter = new BindConverter(
|
||||
ApplicationConversionService.getSharedInstance(), null);
|
||||
assertThat(bindConverter.canConvert("test",
|
||||
ResolvableType.forClass(SampleType.class))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenHasDefaultEditorShouldConvert() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(null);
|
||||
Class<?> converted = bindConverter.convert("java.lang.RuntimeException",
|
||||
ResolvableType.forClass(Class.class));
|
||||
assertThat(converted).isEqualTo(RuntimeException.class);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenHasCustomEditorShouldConvert() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(
|
||||
this::registerSampleTypeEditor);
|
||||
SampleType converted = bindConverter.convert("test",
|
||||
ResolvableType.forClass(SampleType.class));
|
||||
assertThat(converted.getText()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenHasEditorByConventionShouldConvert() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(null);
|
||||
ConventionType converted = bindConverter.convert("test",
|
||||
ResolvableType.forClass(ConventionType.class));
|
||||
assertThat(converted.getText()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenHasEditorForCollectionElementShouldConvert() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(
|
||||
this::registerSampleTypeEditor);
|
||||
List<SampleType> converted = bindConverter.convert("test",
|
||||
ResolvableType.forClassWithGenerics(List.class, SampleType.class));
|
||||
assertThat(converted).isNotEmpty();
|
||||
assertThat(converted.get(0).getText()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenHasEditorForArrayElementShouldConvert() {
|
||||
BindConverter bindConverter = getPropertyEditorOnlyBindConverter(
|
||||
this::registerSampleTypeEditor);
|
||||
SampleType[] converted = bindConverter.convert("test",
|
||||
ResolvableType.forClass(SampleType[].class));
|
||||
assertThat(converted).isNotEmpty();
|
||||
assertThat(converted[0].getText()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenConversionServiceCanConvertShouldConvert() {
|
||||
BindConverter bindConverter = getBindConverter(new SampleTypeConverter());
|
||||
SampleType converted = bindConverter.convert("test",
|
||||
ResolvableType.forClass(SampleType.class));
|
||||
assertThat(converted.getText()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenNotPropertyEditorAndConversionServiceCannotConvertShouldThrowException() {
|
||||
BindConverter bindConverter = new BindConverter(
|
||||
ApplicationConversionService.getSharedInstance(), null);
|
||||
this.thrown.expect(ConverterNotFoundException.class);
|
||||
bindConverter.convert("test", ResolvableType.forClass(SampleType.class));
|
||||
}
|
||||
|
||||
private BindConverter getPropertyEditorOnlyBindConverter(
|
||||
Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
|
||||
return new BindConverter(new ThrowingConversionService(),
|
||||
propertyEditorInitializer);
|
||||
}
|
||||
|
||||
private BindConverter getBindConverter(Converter<?, ?> converter) {
|
||||
GenericConversionService conversionService = new GenericConversionService();
|
||||
conversionService.addConverter(converter);
|
||||
return new BindConverter(conversionService, null);
|
||||
}
|
||||
|
||||
private void registerSampleTypeEditor(PropertyEditorRegistry registry) {
|
||||
registry.registerCustomEditor(SampleType.class, new SampleTypePropertyEditor());
|
||||
}
|
||||
|
||||
static class SampleType {
|
||||
|
||||
private String text;
|
||||
|
||||
public String getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class SampleTypePropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
SampleType value = new SampleType();
|
||||
value.text = text;
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class SampleTypeConverter implements Converter<String, SampleType> {
|
||||
|
||||
@Override
|
||||
public SampleType convert(String source) {
|
||||
SampleType result = new SampleType();
|
||||
result.text = source;
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ConventionType {
|
||||
|
||||
private String text;
|
||||
|
||||
public String getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ConventionTypeEditor extends PropertyEditorSupport {
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
ConventionType value = new ConventionType();
|
||||
value.text = text;
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ConversionService} that always throws an {@link AssertionError}.
|
||||
*/
|
||||
private static class ThrowingConversionService implements ConversionService {
|
||||
|
||||
@Override
|
||||
public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
|
||||
throw new AssertionError("Should not call conversion service");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
throw new AssertionError("Should not call conversion service");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T convert(Object source, Class<T> targetType) {
|
||||
throw new AssertionError("Should not call conversion service");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType,
|
||||
TypeDescriptor targetType) {
|
||||
throw new AssertionError("Should not call conversion service");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
* Copyright 2012-2018 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.
|
||||
|
@ -353,6 +353,27 @@ public class CollectionBinderTests {
|
|||
assertThat(foo.getFoos().get(1).getValue()).isEqualTo("three");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToCollectionShouldUsePropertyEditor() {
|
||||
// gh-12166
|
||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||
source.put("foo[0]", "java.lang.RuntimeException");
|
||||
source.put("foo[1]", "java.lang.IllegalStateException");
|
||||
this.sources.add(source);
|
||||
assertThat(this.binder.bind("foo", Bindable.listOf(Class.class)).get())
|
||||
.containsExactly(RuntimeException.class, IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToCollectionWhenStringShouldUsePropertyEditor() {
|
||||
// gh-12166
|
||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||
source.put("foo", "java.lang.RuntimeException,java.lang.IllegalStateException");
|
||||
this.sources.add(source);
|
||||
assertThat(this.binder.bind("foo", Bindable.listOf(Class.class)).get())
|
||||
.containsExactly(RuntimeException.class, IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToBeanWithNestedCollectionAndNonIterableSourceShouldNotFail() {
|
||||
// gh-10702
|
||||
|
|
|
@ -465,6 +465,17 @@ public class JavaBeanBinderTests {
|
|||
assertThat(bean.getDate().toString()).isEqualTo("2014-04-01");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindWhenValueIsConvertedWithPropertyEditorShouldBind() {
|
||||
// gh-12166
|
||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||
source.put("foo.value", "java.lang.RuntimeException");
|
||||
this.sources.add(source);
|
||||
ExampleWithPropertyEditorType bean = this.binder
|
||||
.bind("foo", Bindable.of(ExampleWithPropertyEditorType.class)).get();
|
||||
assertThat(bean.getValue()).isEqualTo(RuntimeException.class);
|
||||
}
|
||||
|
||||
public static class ExampleValueBean {
|
||||
|
||||
private int intValue;
|
||||
|
@ -825,4 +836,18 @@ public class JavaBeanBinderTests {
|
|||
|
||||
}
|
||||
|
||||
public static class ExampleWithPropertyEditorType {
|
||||
|
||||
private Class<? extends Throwable> value;
|
||||
|
||||
public Class<? extends Throwable> getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public void setValue(Class<? extends Throwable> value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import org.springframework.test.context.support.TestPropertySourceUtils;
|
|||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
|
@ -570,6 +571,30 @@ public class MapBinderTests {
|
|||
this.binder.bind("foo", STRING_STRING_MAP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
public void bindToMapWithPropertyEditorForKey() {
|
||||
// gh-12166
|
||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||
source.put("foo.[java.lang.RuntimeException]", "bar");
|
||||
this.sources.add(source);
|
||||
Map<Class, String> map = this.binder
|
||||
.bind("foo", Bindable.mapOf(Class.class, String.class)).get();
|
||||
assertThat(map).containsExactly(entry(RuntimeException.class, "bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
public void bindToMapWithPropertyEditorForValue() {
|
||||
// gh-12166
|
||||
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||
source.put("foo.bar", "java.lang.RuntimeException");
|
||||
this.sources.add(source);
|
||||
Map<String, Class> map = this.binder
|
||||
.bind("foo", Bindable.mapOf(String.class, Class.class)).get();
|
||||
assertThat(map).containsExactly(entry("bar", RuntimeException.class));
|
||||
}
|
||||
|
||||
private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric,
|
||||
ResolvableType valueType) {
|
||||
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
|
||||
|
|
Loading…
Reference in New Issue