Support binding of YAML style true/false values to 'ON'/'OFF'.
Fixes gh-17798
This commit is contained in:
parent
4928e958ce
commit
835108e522
|
|
@ -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 <T> the source type
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Madhura Bhave
|
||||||
|
*/
|
||||||
|
abstract class AbstractTypeToEnumConverterFactory<T> implements ConverterFactory<T, Enum> {
|
||||||
|
|
||||||
|
private static Map<String, List<String>> ALIASES;
|
||||||
|
|
||||||
|
static {
|
||||||
|
MultiValueMap<String, String> aliases = new LinkedMultiValueMap<>();
|
||||||
|
aliases.add("true", "on");
|
||||||
|
aliases.add("false", "off");
|
||||||
|
ALIASES = Collections.unmodifiableMap(aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Enum> Converter<T, E> getConverter(Class<E> 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 <E extends Enum> Converter<T, E> getTypeToEnumConverter(Class<E> targetType);
|
||||||
|
|
||||||
|
<E extends Enum> E findEnum(String source, Class<E> enumType) {
|
||||||
|
Map<String, E> candidates = new LinkedHashMap<>();
|
||||||
|
for (E candidate : (Set<E>) 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -116,6 +116,7 @@ public class ApplicationConversionService extends FormattingConversionService {
|
||||||
registry.addConverter(new StringToDataSizeConverter());
|
registry.addConverter(new StringToDataSizeConverter());
|
||||||
registry.addConverter(new NumberToDataSizeConverter());
|
registry.addConverter(new NumberToDataSizeConverter());
|
||||||
registry.addConverterFactory(new LenientStringToEnumConverterFactory());
|
registry.addConverterFactory(new LenientStringToEnumConverterFactory());
|
||||||
|
registry.addConverterFactory(new BooleanToEnumConverterFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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<Boolean> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
<E extends Enum> Converter<Boolean, E> getTypeToEnumConverter(Class<E> targetType) {
|
||||||
|
return new BooleanToEnum<>(targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BooleanToEnum<T extends Enum> implements Converter<Boolean, T> {
|
||||||
|
|
||||||
|
private final Class<T> enumType;
|
||||||
|
|
||||||
|
BooleanToEnum(Class<T> enumType) {
|
||||||
|
this.enumType = enumType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T convert(Boolean source) {
|
||||||
|
return findEnum(Boolean.toString(source), this.enumType);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,18 +16,7 @@
|
||||||
|
|
||||||
package org.springframework.boot.convert;
|
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.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.
|
* Converts from a String to a {@link java.lang.Enum} with lenient conversion rules.
|
||||||
|
|
@ -35,31 +24,18 @@ import org.springframework.util.MultiValueMap;
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Uses a case insensitive search</li>
|
* <li>Uses a case insensitive search</li>
|
||||||
* <li>Does not consider {@code '_'}, {@code '$'} or other special characters</li>
|
* <li>Does not consider {@code '_'}, {@code '$'} or other special characters</li>
|
||||||
* <li>Allows mapping of YAML style {@code "false"} and {@code "true"} to enums {@code ON}
|
* <li>Allows mapping of {@code "false"} and {@code "true"} to enums {@code ON} and
|
||||||
* and {@code OFF}</li>
|
* {@code OFF}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
final class LenientStringToEnumConverterFactory implements ConverterFactory<String, Enum> {
|
final class LenientStringToEnumConverterFactory extends AbstractTypeToEnumConverterFactory<String> {
|
||||||
|
|
||||||
private static Map<String, List<String>> ALIASES;
|
|
||||||
static {
|
|
||||||
MultiValueMap<String, String> aliases = new LinkedMultiValueMap<>();
|
|
||||||
aliases.add("true", "on");
|
|
||||||
aliases.add("false", "off");
|
|
||||||
ALIASES = Collections.unmodifiableMap(aliases);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
|
<E extends Enum> Converter<String, E> getTypeToEnumConverter(Class<E> targetType) {
|
||||||
Class<?> enumType = targetType;
|
return new StringToEnum<>(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 class StringToEnum<T extends Enum> implements Converter<String, T> {
|
||||||
|
|
@ -80,36 +56,10 @@ final class LenientStringToEnumConverterFactory implements ConverterFactory<Stri
|
||||||
return (T) Enum.valueOf(this.enumType, source);
|
return (T) Enum.valueOf(this.enumType, source);
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
return findEnum(source);
|
return findEnum(source, this.enumType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private T findEnum(String source) {
|
|
||||||
Map<String, T> candidates = new LinkedHashMap<String, T>();
|
|
||||||
for (T candidate : (Set<T>) 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,15 @@ class SpringApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bindsYamlStyleBannerModeToSpringApplication() {
|
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);
|
SpringApplication application = new SpringApplication(ExampleConfig.class);
|
||||||
application.setWebApplicationType(WebApplicationType.NONE);
|
application.setWebApplicationType(WebApplicationType.NONE);
|
||||||
this.context = application.run("--spring.main.banner-mode=false");
|
this.context = application.run("--spring.main.banner-mode=false");
|
||||||
|
|
|
||||||
|
|
@ -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<? extends Arguments> conversionServices() {
|
||||||
|
return ConversionServiceArguments
|
||||||
|
.with((service) -> service.addConverterFactory(new BooleanToEnumConverterFactory()));
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestOnOffEnum {
|
||||||
|
|
||||||
|
ON, OFF
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestTrueFalseEnum {
|
||||||
|
|
||||||
|
ONE, TWO, TRUE, FALSE, ON, OFF
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue