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