diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java index c5e5fa9319..c25b54f063 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -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 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 convert(Object result, Bindable target) { @@ -65,37 +69,22 @@ class BindConverter { @SuppressWarnings("unchecked") public 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); - } - if (editor == null && String.class != type) { - editor = BeanUtils.findEditorByConvention(type); + 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 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 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 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; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java index 843e30907c..4b78554df9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java @@ -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()); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index 58cf0d2a34..7f903c0108 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -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> list; + + public List> getList() { + return this.list; + } + + public void setList(List> list) { + this.list = list; + } + + } + static class CustomPropertiesValidator implements Validator { @Override diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java index d6bd4e57e5..9b5482296a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ArrayBinderTests.java @@ -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); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindConverterTests.java new file mode 100644 index 0000000000..2ec6380110 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindConverterTests.java @@ -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 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 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 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 { + + @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 convert(Object source, Class 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"); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java index 912409782f..fe4ffc43e7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java @@ -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 diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java index a4328d9350..8f60f6ac3a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -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 value; + + public Class getValue() { + return this.value; + } + + public void setValue(Class value) { + this.value = value; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java index aa009fb7d3..f56fa71667 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -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 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 map = this.binder + .bind("foo", Bindable.mapOf(String.class, Class.class)).get(); + assertThat(map).containsExactly(entry("bar", RuntimeException.class)); + } + private Bindable> getMapBindable(Class keyGeneric, ResolvableType valueType) { ResolvableType keyType = ResolvableType.forClass(keyGeneric);