From 835108e522ca4a1969d35a96ecf731e63f554ea2 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Fri, 9 Aug 2019 16:15:16 -0700 Subject: [PATCH] Support binding of YAML style true/false values to 'ON'/'OFF'. Fixes gh-17798 --- .../AbstractTypeToEnumConverterFactory.java | 89 +++++++++++++++++++ .../convert/ApplicationConversionService.java | 1 + .../BooleanToEnumConverterFactory.java | 50 +++++++++++ .../LenientStringToEnumConverterFactory.java | 62 ++----------- .../boot/SpringApplicationTests.java | 9 ++ .../BooleanToEnumConverterFactoryTests.java | 59 ++++++++++++ 6 files changed, 214 insertions(+), 56 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/AbstractTypeToEnumConverterFactory.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/BooleanToEnumConverterFactory.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/BooleanToEnumConverterFactoryTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/AbstractTypeToEnumConverterFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/AbstractTypeToEnumConverterFactory.java new file mode 100644 index 00000000000..5884d9d1e2e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/AbstractTypeToEnumConverterFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.convert; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Abstract base class for converting from a type to a {@link java.lang.Enum}. + * + * @param the source type + * @author Phillip Webb + * @author Madhura Bhave + */ +abstract class AbstractTypeToEnumConverterFactory implements ConverterFactory { + + private static Map> ALIASES; + + static { + MultiValueMap aliases = new LinkedMultiValueMap<>(); + aliases.add("true", "on"); + aliases.add("false", "off"); + ALIASES = Collections.unmodifiableMap(aliases); + } + + @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 getTypeToEnumConverter(targetType); + } + + abstract Converter getTypeToEnumConverter(Class targetType); + + E findEnum(String source, Class enumType) { + Map candidates = new LinkedHashMap<>(); + for (E candidate : (Set) EnumSet.allOf(enumType)) { + candidates.put(getCanonicalName(candidate.name()), candidate); + } + String name = getCanonicalName(source); + E result = candidates.get(name); + if (result != null) { + return result; + } + for (String alias : ALIASES.getOrDefault(name, Collections.emptyList())) { + result = candidates.get(alias); + if (result != null) { + return result; + } + } + throw new IllegalArgumentException("No enum constant " + enumType.getCanonicalName() + "." + source); + + } + + private String getCanonicalName(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars().filter(Character::isLetterOrDigit).map(Character::toLowerCase) + .forEach((c) -> canonicalName.append((char) c)); + return canonicalName.toString(); + } + +} 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 885009abaa3..0b381d2efe8 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 @@ -116,6 +116,7 @@ public class ApplicationConversionService extends FormattingConversionService { registry.addConverter(new StringToDataSizeConverter()); registry.addConverter(new NumberToDataSizeConverter()); registry.addConverterFactory(new LenientStringToEnumConverterFactory()); + registry.addConverterFactory(new BooleanToEnumConverterFactory()); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/BooleanToEnumConverterFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/BooleanToEnumConverterFactory.java new file mode 100644 index 00000000000..5ae4d9fe32c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/BooleanToEnumConverterFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.convert; + +import org.springframework.core.convert.converter.Converter; + +/** + * Converter to support mapping of YAML style {@code "false"} and {@code "true"} to enums + * {@code ON} and {@code OFF}. + * + * @author Madhura Bhave + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +final class BooleanToEnumConverterFactory extends AbstractTypeToEnumConverterFactory { + + @Override + Converter getTypeToEnumConverter(Class targetType) { + return new BooleanToEnum<>(targetType); + } + + private class BooleanToEnum implements Converter { + + private final Class enumType; + + BooleanToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(Boolean source) { + return findEnum(Boolean.toString(source), this.enumType); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/LenientStringToEnumConverterFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/LenientStringToEnumConverterFactory.java index 070d84d0023..145d7afa902 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/LenientStringToEnumConverterFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/LenientStringToEnumConverterFactory.java @@ -16,18 +16,7 @@ package org.springframework.boot.convert; -import java.util.Collections; -import java.util.EnumSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.ConverterFactory; -import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; /** * Converts from a String to a {@link java.lang.Enum} with lenient conversion rules. @@ -35,31 +24,18 @@ import org.springframework.util.MultiValueMap; *
    *
  • Uses a case insensitive search
  • *
  • Does not consider {@code '_'}, {@code '$'} or other special characters
  • - *
  • Allows mapping of YAML style {@code "false"} and {@code "true"} to enums {@code ON} - * and {@code OFF}
  • + *
  • Allows mapping of {@code "false"} and {@code "true"} to enums {@code ON} and + * {@code OFF}
  • *
* * @author Phillip Webb */ @SuppressWarnings({ "unchecked", "rawtypes" }) -final class LenientStringToEnumConverterFactory implements ConverterFactory { - - private static Map> ALIASES; - static { - MultiValueMap aliases = new LinkedMultiValueMap<>(); - aliases.add("true", "on"); - aliases.add("false", "off"); - ALIASES = Collections.unmodifiableMap(aliases); - } +final class LenientStringToEnumConverterFactory extends AbstractTypeToEnumConverterFactory { @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); + Converter getTypeToEnumConverter(Class targetType) { + return new StringToEnum<>(targetType); } private class StringToEnum implements Converter { @@ -80,36 +56,10 @@ final class LenientStringToEnumConverterFactory implements ConverterFactory candidates = new LinkedHashMap(); - for (T candidate : (Set) EnumSet.allOf(this.enumType)) { - candidates.put(getCanonicalName(candidate.name()), candidate); - } - String name = getCanonicalName(source); - T result = candidates.get(name); - if (result != null) { - return result; - } - for (String alias : ALIASES.getOrDefault(name, Collections.emptyList())) { - result = candidates.get(alias); - if (result != null) { - return result; - } - } - throw new IllegalArgumentException("No enum constant " + this.enumType.getCanonicalName() + "." + source); - } - - private String getCanonicalName(String name) { - StringBuilder canonicalName = new StringBuilder(name.length()); - name.chars().filter(Character::isLetterOrDigit).map(Character::toLowerCase) - .forEach((c) -> canonicalName.append((char) c)); - return canonicalName.toString(); - } - } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 23e2a14b683..52f960a0e49 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -290,6 +290,15 @@ class SpringApplicationTests { @Test void bindsYamlStyleBannerModeToSpringApplication() { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setDefaultProperties(Collections.singletonMap("spring.main.banner-mode", false)); + application.setWebApplicationType(WebApplicationType.NONE); + this.context = application.run(); + assertThat(application).hasFieldOrPropertyWithValue("bannerMode", Banner.Mode.OFF); + } + + @Test + void bindsBooleanAsStringBannerModeToSpringApplication() { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); this.context = application.run("--spring.main.banner-mode=false"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/BooleanToEnumConverterFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/BooleanToEnumConverterFactoryTests.java new file mode 100644 index 00000000000..7fc3d810709 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/BooleanToEnumConverterFactoryTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.convert; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; + +import org.springframework.core.convert.ConversionService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BooleanToEnumConverterFactory}. + * + * @author Madhura Bhave + */ +class BooleanToEnumConverterFactoryTests { + + @ConversionServiceTest + void convertFromBooleanToEnumWhenShouldConvertValue(ConversionService conversionService) { + assertThat(conversionService.convert(true, TestOnOffEnum.class)).isEqualTo(TestOnOffEnum.ON); + assertThat(conversionService.convert(false, TestOnOffEnum.class)).isEqualTo(TestOnOffEnum.OFF); + assertThat(conversionService.convert(true, TestTrueFalseEnum.class)).isEqualTo(TestTrueFalseEnum.TRUE); + assertThat(conversionService.convert(false, TestTrueFalseEnum.class)).isEqualTo(TestTrueFalseEnum.FALSE); + } + + static Stream conversionServices() { + return ConversionServiceArguments + .with((service) -> service.addConverterFactory(new BooleanToEnumConverterFactory())); + } + + enum TestOnOffEnum { + + ON, OFF + + } + + enum TestTrueFalseEnum { + + ONE, TWO, TRUE, FALSE, ON, OFF + + } + +}