diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedConversionService.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedConversionService.java new file mode 100644 index 00000000000..9129c3bad81 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedConversionService.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2014 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.bind; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.util.Assert; + +/** + * Internal {@link ConversionService} used by {@link RelaxedDataBinder} to support + * additional relaxed conversion. + * + * @author Phillip Webb + * @since 1.1.0 + */ +class RelaxedConversionService implements ConversionService { + + private final ConversionService conversionService; + + private final GenericConversionService additionalConverters; + + /** + * Create a new {@link RelaxedConversionService} instance. + * @param conversionService and option root conversion service + */ + public RelaxedConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + this.additionalConverters = new GenericConversionService(); + this.additionalConverters + .addConverterFactory(new StringToEnumIgnoringCaseConverterFactory()); + } + + @Override + public boolean canConvert(Class sourceType, Class targetType) { + return (this.conversionService != null && this.conversionService.canConvert( + sourceType, targetType)) + || this.additionalConverters.canConvert(sourceType, targetType); + } + + @Override + public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (this.conversionService != null && this.conversionService.canConvert( + sourceType, targetType)) + || this.additionalConverters.canConvert(sourceType, targetType); + } + + @Override + @SuppressWarnings("unchecked") + public T convert(Object source, Class targetType) { + Assert.notNull(targetType, "The targetType to convert to cannot be null"); + return (T) convert(source, TypeDescriptor.forObject(source), + TypeDescriptor.valueOf(targetType)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (this.conversionService != null) { + try { + return this.conversionService.convert(source, sourceType, targetType); + } + catch (ConversionFailedException ex) { + // Ignore and try the additional converters + } + } + return this.additionalConverters.convert(source, sourceType, targetType); + } + + /** + * Clone of Spring's package private StringToEnumConverterFactory, but ignoring the + * case of the source. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static class StringToEnumIgnoringCaseConverterFactory implements + ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + Assert.notNull(enumType, "The target type " + targetType.getName() + + " does not refer to an enum"); + return new StringToEnum(enumType); + } + + private class StringToEnum implements Converter { + + private final Class enumType; + + public StringToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + if (source.length() == 0) { + // It's an empty enum identifier: reset the enum value to null. + return null; + } + return (T) Enum.valueOf(this.enumType, source.trim().toUpperCase()); + } + } + + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java index c793736ceb3..5aed920b882 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java @@ -38,6 +38,7 @@ import org.springframework.validation.DataBinder; * case for example). * * @author Dave Syer + * @author Phillip Webb * @see RelaxedNames */ public class RelaxedDataBinder extends DataBinder { @@ -75,6 +76,14 @@ public class RelaxedDataBinder extends DataBinder { this.ignoreNestedProperties = ignoreNestedProperties; } + @Override + public void initBeanPropertyAccess() { + super.initBeanPropertyAccess(); + // Hook in the RelaxedConversionService + getInternalBindingResult().initConversion( + new RelaxedConversionService(getConversionService())); + } + @Override protected void doBind(MutablePropertyValues propertyValues) { propertyValues = modifyProperties(propertyValues, getTarget()); diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java index 3c0f555eaee..bb9e6b525af 100644 --- a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java @@ -52,14 +52,17 @@ import org.springframework.validation.FieldError; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; /** * Tests for {@link RelaxedDataBinder}. * * @author Dave Syer + * @author Phillip Webb */ public class RelaxedDataBinderTests { @@ -419,6 +422,33 @@ public class RelaxedDataBinderTests { assertEquals("efg", c1.get("d1")); } + @Test + public void testBindCaseInsensitiveEnumsWithoutConverter() throws Exception { + VanillaTarget target = new VanillaTarget(); + doTestBindCaseInsensitiveEnums(target); + } + + @Test + public void testBindCaseInsensitiveEnumsWithConverter() throws Exception { + VanillaTarget target = new VanillaTarget(); + this.conversionService = new DefaultConversionService(); + doTestBindCaseInsensitiveEnums(target); + } + + private void doTestBindCaseInsensitiveEnums(VanillaTarget target) throws Exception { + BindingResult result = bind(target, "bingo: THIS"); + assertThat(result.getErrorCount(), equalTo(0)); + assertThat(target.getBingo(), equalTo(Bingo.THIS)); + + result = bind(target, "bingo: oR"); + assertThat(result.getErrorCount(), equalTo(0)); + assertThat(target.getBingo(), equalTo(Bingo.OR)); + + result = bind(target, "bingo: that"); + assertThat(result.getErrorCount(), equalTo(0)); + assertThat(target.getBingo(), equalTo(Bingo.THAT)); + } + private BindingResult bind(Object target, String values) throws Exception { return bind(target, values, null); } @@ -672,6 +702,8 @@ public class RelaxedDataBinderTests { private String fooBaz; + private Bingo bingo; + public char[] getBar() { return this.bar; } @@ -712,6 +744,18 @@ public class RelaxedDataBinderTests { this.fooBaz = fooBaz; } + public Bingo getBingo() { + return this.bingo; + } + + public void setBingo(Bingo bingo) { + this.bingo = bingo; + } + + } + + static enum Bingo { + THIS, OR, THAT } public static class ValidatedTarget { diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java index 9171e55a22f..a434b84c92a 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java @@ -32,15 +32,17 @@ import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; /** - * * Tests for {@link ConfigurationPropertiesBindingPostProcessor}. * * @author Christian Dupuis + * @author Phillip Webb */ public class ConfigurationPropertiesBindingPostProcessorTests { @@ -117,6 +119,16 @@ public class ConfigurationPropertiesBindingPostProcessorTests { this.context.refresh(); } + @Test + public void testPropertyWithEnum() throws Exception { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, "test.value:foo"); + this.context.register(PropertyWithEnum.class); + this.context.refresh(); + assertThat(this.context.getBean(PropertyWithEnum.class).getValue(), + equalTo(FooEnum.FOO)); + } + @Configuration @EnableConfigurationProperties public static class TestConfigurationWithValidatingSetter { @@ -228,6 +240,27 @@ public class ConfigurationPropertiesBindingPostProcessorTests { public String getBar() { return this.bar; } + } + @Configuration + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test") + public static class PropertyWithEnum { + + private FooEnum value; + + public void setValue(FooEnum value) { + this.value = value; + } + + public FooEnum getValue() { + return this.value; + } + + } + + static enum FooEnum { + FOO, BAZ, BAR + } }