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 NumberToDataSizeConverter()); | ||||
| 		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; | ||||
| 
 | ||||
| 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; | |||
|  * <ul> | ||||
|  * <li>Uses a case insensitive search</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} | ||||
|  * and {@code OFF}</li> | ||||
|  * <li>Allows mapping of {@code "false"} and {@code "true"} to enums {@code ON} and | ||||
|  * {@code OFF}</li> | ||||
|  * </ul> | ||||
|  * | ||||
|  * @author Phillip Webb | ||||
|  */ | ||||
| @SuppressWarnings({ "unchecked", "rawtypes" }) | ||||
| final class LenientStringToEnumConverterFactory implements ConverterFactory<String, 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); | ||||
| 	} | ||||
| final class LenientStringToEnumConverterFactory extends AbstractTypeToEnumConverterFactory<String> { | ||||
| 
 | ||||
| 	@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); | ||||
| 	<E extends Enum> Converter<String, E> getTypeToEnumConverter(Class<E> targetType) { | ||||
| 		return new StringToEnum<>(targetType); | ||||
| 	} | ||||
| 
 | ||||
| 	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); | ||||
| 			} | ||||
| 			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 | ||||
| 	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"); | ||||
|  |  | |||
|  | @ -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