From c2e16768495d2e6223947e4112d5e8cb5a6f4d88 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 30 May 2014 15:33:10 +0100 Subject: [PATCH] Allow case insensitive enums in RelaxedDataBinder Update RelaxedDataBinder to hook in an additional RelaxedConversionService which can deal with case insensitive enum mappings. The RelaxedConversionService could be extended in the future with additional converters if necessary. Fixes gh-996 --- .../boot/bind/RelaxedConversionService.java | 125 ++++++++++++++++++ .../boot/bind/RelaxedDataBinder.java | 9 ++ .../boot/bind/RelaxedDataBinderTests.java | 44 ++++++ ...onPropertiesBindingPostProcessorTests.java | 35 ++++- 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/bind/RelaxedConversionService.java 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 + } }