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
pull/12202/head
Phillip Webb 7 years ago
parent 4b9c3c137e
commit 3dea6fc645

@ -19,15 +19,20 @@ 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.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.SimpleTypeConverter;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor; 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; import org.springframework.util.Assert;
/** /**
@ -38,25 +43,24 @@ import org.springframework.util.Assert;
*/ */
class BindConverter { class BindConverter {
private final ConversionService conversionService; private final ConversionService typeConverterConversionService;
private final SimpleTypeConverter simpleTypeConverter; 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(
propertyEditorInitializer);
this.conversionService = conversionService; this.conversionService = conversionService;
this.simpleTypeConverter = new SimpleTypeConverter();
if (propertyEditorInitializer != null) {
propertyEditorInitializer.accept(this.simpleTypeConverter);
}
} }
public boolean canConvert(Object value, ResolvableType type, public boolean canConvert(Object value, ResolvableType type,
Annotation... annotations) { Annotation... annotations) {
return getPropertyEditor(type.resolve()) != null TypeDescriptor sourceType = TypeDescriptor.forObject(value);
|| this.conversionService.canConvert(TypeDescriptor.forObject(value), TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations);
new ResolvableTypeDescriptor(type, annotations)); return this.typeConverterConversionService.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) {
@ -65,37 +69,22 @@ class BindConverter {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> T convert(Object value, ResolvableType type, Annotation... annotations) { public <T> T convert(Object value, ResolvableType type, Annotation... annotations) {
PropertyEditor propertyEditor = getPropertyEditor(type.resolve()); if (value == null) {
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)) {
return null; return null;
} }
PropertyEditor editor = this.simpleTypeConverter.getDefaultEditor(type); TypeDescriptor sourceType = TypeDescriptor.forObject(value);
if (editor == null) { TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations);
editor = this.simpleTypeConverter.findCustomEditor(type, null); if (this.typeConverterConversionService.canConvert(sourceType, targetType)) {
} return (T) this.typeConverterConversionService.convert(value, sourceType,
if (editor == null && String.class != type) { targetType);
editor = BeanUtils.findEditorByConvention(type);
} }
return editor; return (T) this.conversionService.convert(value, sourceType, targetType);
} }
/** /**
* A {@link TypeDescriptor} backed by a {@link ResolvableType}. * A {@link TypeDescriptor} backed by a {@link ResolvableType}.
*/ */
final class ResolvableTypeDescriptor extends TypeDescriptor { private static class ResolvableTypeDescriptor extends TypeDescriptor {
ResolvableTypeDescriptor(ResolvableType resolvableType, ResolvableTypeDescriptor(ResolvableType resolvableType,
Annotation[] annotations) { 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.ConversionService;
import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.FormatterRegistry; import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.DefaultFormattingConversionService;
@ -48,10 +49,7 @@ public class ApplicationConversionService extends FormattingConversionService {
if (embeddedValueResolver != null) { if (embeddedValueResolver != null) {
setEmbeddedValueResolver(embeddedValueResolver); setEmbeddedValueResolver(embeddedValueResolver);
} }
DefaultConversionService.addDefaultConverters(this); configure(this);
DefaultFormattingConversionService.addDefaultFormatters(this);
addApplicationConverters(this);
addApplicationFormatters(this);
} }
/** /**
@ -73,12 +71,31 @@ public class ApplicationConversionService extends FormattingConversionService {
return sharedInstance; return sharedInstance;
} }
public void addApplicationConverters(ConverterRegistry registry) { /**
ConversionService service = (ConversionService) registry; * Configure the given {@link FormatterRegistry} with formatters and converts
registry.addConverter(new ArrayToDelimitedStringConverter(service)); * appropriate for most Spring Boot applications.
registry.addConverter(new CollectionToDelimitedStringConverter(service)); * @param registry the registry of converters to add to (must also be castable to
registry.addConverter(new DelimitedStringToArrayConverter(service)); * ConversionService, e.g. being a {@link ConfigurableConversionService})
registry.addConverter(new DelimitedStringToCollectionConverter(service)); * @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 StringToDurationConverter());
registry.addConverter(new DurationToStringConverter()); registry.addConverter(new DurationToStringConverter());
registry.addConverter(new NumberToDurationConverter()); registry.addConverter(new NumberToDurationConverter());
@ -86,7 +103,26 @@ public class ApplicationConversionService extends FormattingConversionService {
registry.addConverterFactory(new StringToEnumIgnoringCaseConverterFactory()); 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 CharArrayFormatter());
registry.addFormatter(new InetAddressFormatter()); registry.addFormatter(new InetAddressFormatter());
registry.addFormatter(new IsoOffsetFormatter()); registry.addFormatter(new IsoOffsetFormatter());

@ -728,6 +728,15 @@ public class ConfigurationPropertiesTests {
assertThat(bean.getPerson().lastName).isEqualTo("boot"); 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, private AnnotationConfigApplicationContext load(Class<?> configuration,
String... inlinedProperties) { String... inlinedProperties) {
return load(new Class<?>[] { configuration }, 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 { static class CustomPropertiesValidator implements Validator {
@Override @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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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"); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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"); 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 @Test
public void bindToBeanWithNestedCollectionAndNonIterableSourceShouldNotFail() { public void bindToBeanWithNestedCollectionAndNonIterableSourceShouldNotFail() {
// gh-10702 // gh-10702

@ -465,6 +465,17 @@ public class JavaBeanBinderTests {
assertThat(bean.getDate().toString()).isEqualTo("2014-04-01"); 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 { public static class ExampleValueBean {
private int intValue; 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 org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat; 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.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isA;
@ -570,6 +571,30 @@ public class MapBinderTests {
this.binder.bind("foo", STRING_STRING_MAP); 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, private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric,
ResolvableType valueType) { ResolvableType valueType) {
ResolvableType keyType = ResolvableType.forClass(keyGeneric); ResolvableType keyType = ResolvableType.forClass(keyGeneric);

Loading…
Cancel
Save