Commit c2e16768 authored by Phillip Webb's avatar Phillip Webb

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
parent 2fec9c47
/*
* 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> T convert(Object source, Class<T> 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<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> 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<T extends Enum> implements Converter<String, T> {
private final Class<T> enumType;
public StringToEnum(Class<T> 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());
}
}
}
}
...@@ -38,6 +38,7 @@ import org.springframework.validation.DataBinder; ...@@ -38,6 +38,7 @@ import org.springframework.validation.DataBinder;
* case for example). * case for example).
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb
* @see RelaxedNames * @see RelaxedNames
*/ */
public class RelaxedDataBinder extends DataBinder { public class RelaxedDataBinder extends DataBinder {
...@@ -75,6 +76,14 @@ public class RelaxedDataBinder extends DataBinder { ...@@ -75,6 +76,14 @@ public class RelaxedDataBinder extends DataBinder {
this.ignoreNestedProperties = ignoreNestedProperties; this.ignoreNestedProperties = ignoreNestedProperties;
} }
@Override
public void initBeanPropertyAccess() {
super.initBeanPropertyAccess();
// Hook in the RelaxedConversionService
getInternalBindingResult().initConversion(
new RelaxedConversionService(getConversionService()));
}
@Override @Override
protected void doBind(MutablePropertyValues propertyValues) { protected void doBind(MutablePropertyValues propertyValues) {
propertyValues = modifyProperties(propertyValues, getTarget()); propertyValues = modifyProperties(propertyValues, getTarget());
......
...@@ -52,14 +52,17 @@ import org.springframework.validation.FieldError; ...@@ -52,14 +52,17 @@ import org.springframework.validation.FieldError;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
/** /**
* Tests for {@link RelaxedDataBinder}. * Tests for {@link RelaxedDataBinder}.
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb
*/ */
public class RelaxedDataBinderTests { public class RelaxedDataBinderTests {
...@@ -419,6 +422,33 @@ public class RelaxedDataBinderTests { ...@@ -419,6 +422,33 @@ public class RelaxedDataBinderTests {
assertEquals("efg", c1.get("d1")); 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 { private BindingResult bind(Object target, String values) throws Exception {
return bind(target, values, null); return bind(target, values, null);
} }
...@@ -672,6 +702,8 @@ public class RelaxedDataBinderTests { ...@@ -672,6 +702,8 @@ public class RelaxedDataBinderTests {
private String fooBaz; private String fooBaz;
private Bingo bingo;
public char[] getBar() { public char[] getBar() {
return this.bar; return this.bar;
} }
...@@ -712,6 +744,18 @@ public class RelaxedDataBinderTests { ...@@ -712,6 +744,18 @@ public class RelaxedDataBinderTests {
this.fooBaz = fooBaz; 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 { public static class ValidatedTarget {
......
...@@ -32,15 +32,17 @@ import org.springframework.validation.Errors; ...@@ -32,15 +32,17 @@ import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils; import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
/** /**
*
* Tests for {@link ConfigurationPropertiesBindingPostProcessor}. * Tests for {@link ConfigurationPropertiesBindingPostProcessor}.
* *
* @author Christian Dupuis * @author Christian Dupuis
* @author Phillip Webb
*/ */
public class ConfigurationPropertiesBindingPostProcessorTests { public class ConfigurationPropertiesBindingPostProcessorTests {
...@@ -117,6 +119,16 @@ public class ConfigurationPropertiesBindingPostProcessorTests { ...@@ -117,6 +119,16 @@ public class ConfigurationPropertiesBindingPostProcessorTests {
this.context.refresh(); 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 @Configuration
@EnableConfigurationProperties @EnableConfigurationProperties
public static class TestConfigurationWithValidatingSetter { public static class TestConfigurationWithValidatingSetter {
...@@ -228,6 +240,27 @@ public class ConfigurationPropertiesBindingPostProcessorTests { ...@@ -228,6 +240,27 @@ public class ConfigurationPropertiesBindingPostProcessorTests {
public String getBar() { public String getBar() {
return this.bar; 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
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment