/*
* Copyright 2012-2017 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.context.properties;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.validation.constraints.NotNull;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.boot.context.properties.bind.validation.BindValidationException;
import org.springframework.boot.testutil.InternalOutputCapture;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.SystemEnvironmentPropertySource;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
/**
* Tests for {@link ConfigurationPropertiesBindingPostProcessor}.
*
* @author Christian Dupuis
* @author Phillip Webb
* @author Stephane Nicoll
* @author Madhura Bhave
*/
public class ConfigurationPropertiesBindingPostProcessorTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public InternalOutputCapture output = new InternalOutputCapture();
private AnnotationConfigApplicationContext context;
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void testValidationWithSetter() {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"test.foo=spam");
this.context.register(TestConfigurationWithValidatingSetter.class);
try {
this.context.refresh();
fail("Expected exception");
}
catch (BeanCreationException ex) {
BindException bindException = (BindException) ex.getCause();
assertThat(bindException.getMessage())
.startsWith("Failed to bind properties under 'test' to "
+ PropertyWithValidatingSetter.class.getName());
}
}
@Test
public void unknownFieldFailureMessageContainsDetailsOfPropertyOrigin() {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"com.example.baz=spam");
this.context.register(TestConfiguration.class);
try {
this.context.refresh();
fail("Expected exception");
}
catch (BeanCreationException ex) {
BindException bindException = (BindException) ex.getCause();
assertThat(bindException.getMessage())
.startsWith("Failed to bind properties under 'com.example' to "
+ TestConfiguration.class.getName());
}
}
@Test
public void testValidationWithoutJSR303() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(TestConfigurationWithoutJSR303.class);
assertBindingFailure(1);
}
@Test
public void testValidationWithJSR303() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(TestConfigurationWithJSR303.class);
assertBindingFailure(2);
}
@Test
public void testValidationAndNullOutValidator() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(TestConfiguration.class);
this.context.refresh();
ConfigurationPropertiesBindingPostProcessor bean = this.context
.getBean(ConfigurationPropertiesBindingPostProcessor.class);
assertThat(ReflectionTestUtils.getField(bean, "validator")).isNull();
}
@Test
public void testSuccessfulValidationWithJSR303() {
MockEnvironment env = new MockEnvironment();
env.setProperty("test.foo", "123456");
env.setProperty("test.bar", "654321");
this.context = new AnnotationConfigApplicationContext();
this.context.setEnvironment(env);
this.context.register(TestConfigurationWithJSR303.class);
this.context.refresh();
}
@Test
public void testSuccessfulValidationWithInterface() {
MockEnvironment env = new MockEnvironment();
env.setProperty("test.foo", "bar");
this.context = new AnnotationConfigApplicationContext();
this.context.setEnvironment(env);
this.context.register(TestConfigurationWithValidationAndInterface.class);
this.context.refresh();
assertThat(this.context.getBean(ValidatedPropertiesImpl.class).getFoo())
.isEqualTo("bar");
}
@Test
public void testInitializersSeeBoundProperties() {
MockEnvironment env = new MockEnvironment();
env.setProperty("bar", "foo");
this.context = new AnnotationConfigApplicationContext();
this.context.setEnvironment(env);
this.context.register(TestConfigurationWithInitializer.class);
this.context.refresh();
}
@Test
public void testValidationWithCustomValidator() {
this.context = new AnnotationConfigApplicationContext();
this.context.register(TestConfigurationWithCustomValidator.class);
assertBindingFailure(1);
}
@Test
public void testValidationWithCustomValidatorNotSupported() {
MockEnvironment env = new MockEnvironment();
env.setProperty("test.foo", "bar");
this.context = new AnnotationConfigApplicationContext();
this.context.setEnvironment(env);
this.context.register(TestConfigurationWithCustomValidator.class,
PropertyWithValidatingSetter.class);
assertBindingFailure(1);
}
@Test
public void testPropertyWithEnum() throws Exception {
doEnumTest("test.theValue=foo");
}
@Test
public void testRelaxedPropertyWithEnum() throws Exception {
doEnumTest("test.the-value=FoO");
doEnumTest("test.THE_VALUE=FoO");
}
private void doEnumTest(String property) {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, property);
this.context.register(PropertyWithEnum.class);
this.context.refresh();
assertThat(this.context.getBean(PropertyWithEnum.class).getTheValue())
.isEqualTo(FooEnum.FOO);
this.context.close();
}
@Test
public void testRelaxedPropertyWithSetOfEnum() {
doEnumSetTest("test.the-values=foo,bar", FooEnum.FOO, FooEnum.BAR);
doEnumSetTest("test.the-values=foo", FooEnum.FOO);
}
private void doEnumSetTest(String property, FooEnum... expected) {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, property);
this.context.register(PropertyWithEnum.class);
this.context.refresh();
assertThat(this.context.getBean(PropertyWithEnum.class).getTheValues())
.contains(expected);
this.context.close();
}
@Test
public void testValueBindingForDefaults() throws Exception {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"default.value=foo");
this.context.register(PropertyWithValue.class);
this.context.refresh();
assertThat(this.context.getBean(PropertyWithValue.class).getValue())
.isEqualTo("foo");
}
@Test
public void configurationPropertiesWithFactoryBean() throws Exception {
ConfigurationPropertiesWithFactoryBean.factoryBeanInit = false;
this.context = new AnnotationConfigApplicationContext() {
@Override
protected void onRefresh() throws BeansException {
assertThat(ConfigurationPropertiesWithFactoryBean.factoryBeanInit)
.as("Init too early").isFalse();
super.onRefresh();
}
};
this.context.register(ConfigurationPropertiesWithFactoryBean.class);
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(FactoryBeanTester.class);
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
this.context.registerBeanDefinition("test", beanDefinition);
this.context.refresh();
assertThat(ConfigurationPropertiesWithFactoryBean.factoryBeanInit).as("No init")
.isTrue();
}
@Test
public void configurationPropertiesWithCharArray() throws Exception {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"test.chars=word");
this.context.register(PropertyWithCharArray.class);
this.context.refresh();
assertThat(this.context.getBean(PropertyWithCharArray.class).getChars())
.isEqualTo("word".toCharArray());
}
@Test
public void notWritablePropertyException() throws Exception {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"test.madeup:word");
this.context.register(PropertyWithCharArray.class);
this.thrown.expect(BeanCreationException.class);
this.thrown.expectMessage("test");
this.context.refresh();
}
@Test
public void relaxedPropertyNamesSame() throws Exception {
testRelaxedPropertyNames("test.FOO_BAR=test1", "test.FOO_BAR=test2",
"test.BAR-B-A-Z=testa", "test.BAR-B-A-Z=testb");
}
private void testRelaxedPropertyNames(String... environment) {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
environment);
this.context.register(RelaxedPropertyNames.class);
this.context.refresh();
RelaxedPropertyNames bean = this.context.getBean(RelaxedPropertyNames.class);
assertThat(bean.getFooBar()).isEqualTo("test2");
assertThat(bean.getBarBAZ()).isEqualTo("testb");
}
@Test
public void nestedProperties() throws Exception {
// gh-3539
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"test.nested.value=test1");
this.context.register(PropertyWithNestedValue.class);
this.context.refresh();
assertThat(this.context.getBean(PropertyWithNestedValue.class).getNested()
.getValue()).isEqualTo("test1");
}
@Test
public void bindWithoutConfigurationPropertiesAnnotation() {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"name:foo");
this.context.register(ConfigurationPropertiesWithoutAnnotation.class);
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("No ConfigurationProperties annotation found");
this.context.refresh();
}
@Test
public void bindWithIgnoreInvalidFieldsAnnotation() {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"com.example.bar=spam");
this.context.register(TestConfigurationWithIgnoreErrors.class);
this.context.refresh();
assertThat(this.context.getBean(TestConfigurationWithIgnoreErrors.class).getBar())
.isEqualTo(0);
}
@Test
public void bindWithNoIgnoreInvalidFieldsAnnotation() {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"com.example.foo=hello");
this.context.register(TestConfiguration.class);
this.thrown.expect(BeanCreationException.class);
this.context.refresh();
}
@Test
public void multiplePropertySourcesPlaceholderConfigurer() throws Exception {
this.context = new AnnotationConfigApplicationContext();
this.context.register(MultiplePropertySourcesPlaceholderConfigurer.class);
this.context.refresh();
assertThat(this.output.toString()).contains(
"Multiple PropertySourcesPlaceholderConfigurer beans registered");
}
@Test
public void propertiesWithMap() throws Exception {
this.context = new AnnotationConfigApplicationContext();
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
"test.map.foo=bar");
this.context.register(PropertiesWithMap.class);
this.context.refresh();
assertThat(this.context.getBean(PropertiesWithMap.class).getMap())
.containsEntry("foo", "bar");
}
@Test
public void systemPropertiesShouldBindToMap() throws Exception {
MockEnvironment env = new MockEnvironment();
MutablePropertySources propertySources = env.getPropertySources();
propertySources.addLast(new SystemEnvironmentPropertySource("system",
Collections.singletonMap("TEST_MAP_FOO_BAR", "baz")));
this.context = new AnnotationConfigApplicationContext();
this.context.setEnvironment(env);
this.context.register(PropertiesWithComplexMap.class);
this.context.refresh();
Map<String, Map<String, String>> map = this.context
.getBean(PropertiesWithComplexMap.class).getMap();
Map<String, String> foo = map.get("foo");
assertThat(foo).containsEntry("bar", "baz");
}
@Test
public void overridingPropertiesInEnvShouldOverride() throws Exception {
this.context = new AnnotationConfigApplicationContext();
ConfigurableEnvironment env = this.context.getEnvironment();
MutablePropertySources propertySources = env.getPropertySources();
propertySources.addFirst(new SystemEnvironmentPropertySource("system",
Collections.singletonMap("COM_EXAMPLE_FOO", "10")));
propertySources.addLast(new MapPropertySource("test",
Collections.singletonMap("com.example.foo", 5)));
this.context.register(TestConfiguration.class);
this.context.refresh();
int foo = this.context.getBean(TestConfiguration.class).getFoo();
assertThat(foo).isEqualTo(10);
}
@Test
public void overridingPropertiesWithPlaceholderResolutionInEnvShouldOverride()
throws Exception {
this.context = new AnnotationConfigApplicationContext();
ConfigurableEnvironment env = this.context.getEnvironment();
MutablePropertySources propertySources = env.getPropertySources();
propertySources.addFirst(new SystemEnvironmentPropertySource("system",
Collections.singletonMap("COM_EXAMPLE_BAR", "10")));
Map<String, Object> source = new HashMap<>();
source.put("com.example.bar", 5);
source.put("com.example.foo", "${com.example.bar}");
propertySources.addLast(new MapPropertySource("test", source));
this.context.register(TestConfiguration.class);
this.context.refresh();
int foo = this.context.getBean(TestConfiguration.class).getFoo();
assertThat(foo).isEqualTo(10);
}
private void assertBindingFailure(int errorCount) {
try {
this.context.refresh();
fail("Expected exception");
}
catch (BeanCreationException ex) {
assertThat(((BindValidationException) ex.getRootCause()).getValidationErrors()
.getAllErrors().size()).isEqualTo(errorCount);
}
}
@Configuration
@EnableConfigurationProperties
public static class TestConfigurationWithValidatingSetter {
@Bean
public PropertyWithValidatingSetter testProperties() {
return new PropertyWithValidatingSetter();
}
}
@ConfigurationProperties(prefix = "test")
public static class PropertyWithValidatingSetter {
private String foo;
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
if (!foo.equals("bar")) {
throw new IllegalArgumentException("Wrong value for foo");
}
}
}
@Configuration
@EnableConfigurationProperties
public static class TestConfigurationWithoutJSR303 {
@Bean
public PropertyWithoutJSR303 testProperties() {
return new PropertyWithoutJSR303();
}
}
@ConfigurationProperties(prefix = "test")
@Validated
public static class PropertyWithoutJSR303 implements Validator {
private String foo;
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(getClass());
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "foo", "TEST1");
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
}
@Configuration
@EnableConfigurationProperties
public static class TestConfigurationWithJSR303 {
@Bean
public PropertyWithJSR303 testProperties() {
return new PropertyWithJSR303();
}
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties
public static class TestConfigurationWithInitializer {
private String bar;
public void setBar(String bar) {
this.bar = bar;
}
public String getBar() {
return this.bar;
}
@PostConstruct
public void init() {
assertThat(this.bar).isNotNull();
}
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "com.example", ignoreUnknownFields = false)
public static class TestConfiguration {
private int foo;
private String bar;
public void setBar(String bar) {
this.bar = bar;
}
public String getBar() {
return this.bar;
}
public int getFoo() {
return this.foo;
}
public void setFoo(int foo) {
this.foo = foo;
}
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "com.example", ignoreInvalidFields = true)
public static class TestConfigurationWithIgnoreErrors {
private long bar;
public void setBar(long bar) {
this.bar = bar;
}
public long getBar() {
return this.bar;
}
}
@ConfigurationProperties(prefix = "test")
@Validated
public static class PropertyWithJSR303 extends PropertyWithoutJSR303 {
@NotNull
private String bar;
public void setBar(String bar) {
this.bar = bar;
}
public String getBar() {
return this.bar;
}
}
@Configuration
@EnableConfigurationProperties
public static class TestConfigurationWithValidationAndInterface {
@Bean
public ValidatedPropertiesImpl testProperties() {
return new ValidatedPropertiesImpl();
}
}
interface ValidatedProperties {
String getFoo();
}
@ConfigurationProperties("test")
@Validated
public static class ValidatedPropertiesImpl implements ValidatedProperties {
@NotNull
private String foo;
@Override
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
}
@Configuration
@EnableConfigurationProperties
public static class TestConfigurationWithCustomValidator {
@Bean
public PropertyWithCustomValidator propertyWithCustomValidator() {
return new PropertyWithCustomValidator();
}
@Bean
public Validator configurationPropertiesValidator() {
return new CustomPropertyValidator();
}
}
@ConfigurationProperties(prefix = "custom")
@Validated
public static class PropertyWithCustomValidator {
private String foo;
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
}
public static class CustomPropertyValidator implements Validator {
@Override
public boolean supports(Class<?> aClass) {
return aClass == PropertyWithCustomValidator.class;
}
@Override
public void validate(Object o, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "foo", "TEST1");
}
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test", ignoreUnknownFields = false)
public static class PropertyWithCharArray {
private char[] chars;
public char[] getChars() {
return this.chars;
}
public void setChars(char[] chars) {
this.chars = chars;
}
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test", ignoreUnknownFields = false)
public static class PropertyWithCharArrayExpansion {
private char[] chars = new char[] { 'w', 'o', 'r', 'd' };
public char[] getChars() {
return this.chars;
}
public void setChars(char[] chars) {
this.chars = chars;
}
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test")
public static class PropertyWithEnum {
private FooEnum theValue;
private List<FooEnum> theValues;
public void setTheValue(FooEnum value) {
this.theValue = value;
}
public FooEnum getTheValue() {
return this.theValue;
}
public List<FooEnum> getTheValues() {
return this.theValues;
}
public void setTheValues(List<FooEnum> theValues) {
this.theValues = theValues;
}
}
enum FooEnum {
FOO, BAZ, BAR
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test")
@Validated
public static class PropertyWithValue {
@Value("${default.value}")
private String value;
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
@Bean
public static PropertySourcesPlaceholderConfigurer configurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
@Configuration
@EnableConfigurationProperties
@Validated
@ConfigurationProperties(prefix = "test")
public static class PropertiesWithMap {
@Bean
public Validator validator() {
return new LocalValidatorFactoryBean();
}
private Map<String, String> map;
public Map<String, String> getMap() {
return this.map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test")
public static class PropertiesWithComplexMap {
private Map<String, Map<String, String>> map;
public Map<String, Map<String, String>> getMap() {
return this.map;
}
public void setMap(Map<String, Map<String, String>> map) {
this.map = map;
}
}
@Configuration
@EnableConfigurationProperties
public static class ConfigurationPropertiesWithFactoryBean {
public static boolean factoryBeanInit;
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test")
public static class RelaxedPropertyNames {
private String fooBar;
private String barBAZ;
public String getFooBar() {
return this.fooBar;
}
public void setFooBar(String fooBar) {
this.fooBar = fooBar;
}
public String getBarBAZ() {
return this.barBAZ;
}
public void setBarBAZ(String barBAZ) {
this.barBAZ = barBAZ;
}
}
@SuppressWarnings("rawtypes")
// Must be a raw type
static class FactoryBeanTester implements FactoryBean, InitializingBean {
@Override
public Object getObject() throws Exception {
return Object.class;
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void afterPropertiesSet() throws Exception {
ConfigurationPropertiesWithFactoryBean.factoryBeanInit = true;
}
}
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test")
public static class PropertyWithNestedValue {
private Nested nested = new Nested();
public Nested getNested() {
return this.nested;
}
@Bean
public static PropertySourcesPlaceholderConfigurer configurer() {
return new PropertySourcesPlaceholderConfigurer();
}
public static class Nested {
@Value("${default.value}")
private String value;
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
}
@Configuration
@EnableConfigurationProperties(PropertyWithoutConfigurationPropertiesAnnotation.class)
public static class ConfigurationPropertiesWithoutAnnotation {
}
@Configuration
@EnableConfigurationProperties
public static class MultiplePropertySourcesPlaceholderConfigurer {
@Bean
public static PropertySourcesPlaceholderConfigurer configurer1() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public static PropertySourcesPlaceholderConfigurer configurer2() {
return new PropertySourcesPlaceholderConfigurer();
}
}
public static class PropertyWithoutConfigurationPropertiesAnnotation {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
}