From 1d83e87bcdcd0a70680ac834900ddd3821cc65b9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 14 Feb 2018 17:36:55 -0800 Subject: [PATCH] Validate @ConfigurationProperties on @Bean methods Refactor `ConfigurationPropertiesBindingPostProcessor` to allow JSR-303 validation on `@ConfigurationProperties` defined at the `@Bean` method level. JSR-303 validation is now applied when a JSR-303 implementation is available and `@Validated` is present on either the configuration properties class itself or the `@Bean` method that creates it. Standard Spring validation is also supported using a validator bean named `configurationPropertiesValidator`, or by having the configuration properties implement `Validator`. The commit also consolidates tests into a single location. Fixes gh-10803 --- .../main/asciidoc/spring-boot-features.adoc | 10 +- .../ConfigurationBeanFactoryMetadata.java | 6 + .../ConfigurationPropertiesBindException.java | 69 + .../ConfigurationPropertiesBinder.java | 162 +- .../ConfigurationPropertiesBinderBuilder.java | 189 -- ...nfigurationPropertiesBindingException.java | 41 - ...urationPropertiesBindingPostProcessor.java | 183 +- ...opertiesBindingPostProcessorRegistrar.java | 42 +- ...nfigurationPropertiesJsr303Validator.java} | 25 +- .../properties/ConversionServiceDeducer.java | 98 + ...ConfigurationPropertiesImportSelector.java | 117 +- .../properties/PropertySourcesDeducer.java | 95 + .../context/properties/bind/Bindable.java | 18 +- .../validation/BindValidationException.java | 11 + .../validation/ValidationBindHandler.java | 42 +- .../bind/validation/ValidationErrors.java | 4 +- ...igurationPropertiesBinderBuilderTests.java | 195 -- .../ConfigurationPropertiesBinderTests.java | 485 ----- ...onPropertiesBindingPostProcessorTests.java | 689 -------- .../ConfigurationPropertiesTests.java | 1572 +++++++++++++++++ .../EnableConfigurationPropertiesTests.java | 679 ------- .../properties/bind/BindableTests.java | 17 +- .../ValidationBindHandlerTests.java | 18 +- .../context/properties/testProperties.xml | 4 +- 24 files changed, 2132 insertions(+), 2639 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderBuilder.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingException.java rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/{Jsr303ConfigurationPropertiesValidator.java => ConfigurationPropertiesJsr303Validator.java} (67%) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertySourcesDeducer.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderBuilderTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index b2178c8177..50439f7ef2 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1213,9 +1213,13 @@ to your fields, as shown in the following example: } ---- -In order to validate the values of nested properties, you must annotate the associated -field as `@Valid` to trigger its validation. The following example builds on the -preceding `AcmeProperties` example: +TIP: You can also trigger validation by annotating the `@Bean` method that creates the +configuration properties with `@Validated`. + +Although nested properties will also be validated when bound, it's good practice to +also annotate the associated field as `@Valid`. This ensure that validation is triggered +even if no nested properties are found. The following example builds on the preceding +`AcmeProperties` example: [source,java,indent=0] ---- diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java index a11832d251..f27a3c2d75 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata.java @@ -39,6 +39,12 @@ import org.springframework.util.ReflectionUtils; */ public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor { + /** + * The bean name that this class is registered with. + */ + public static final String BEAN_NAME = ConfigurationBeanFactoryMetadata.class + .getName(); + private ConfigurableListableBeanFactory beanFactory; private final Map beansFactoryMetadata = new HashMap<>(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java new file mode 100644 index 0000000000..2c475107e1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindException.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2018 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 org.springframework.beans.factory.BeanCreationException; +import org.springframework.util.ClassUtils; + +/** + * Exception thrown when {@link ConfigurationProperties @ConfigurationProperties} binding + * fails. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class ConfigurationPropertiesBindException extends BeanCreationException { + + private final Class beanType; + + private final ConfigurationProperties annotation; + + ConfigurationPropertiesBindException(String beanName, Object bean, + ConfigurationProperties annotation, Exception cause) { + super(beanName, getMessage(bean, annotation), cause); + this.beanType = bean.getClass(); + this.annotation = annotation; + } + + /** + * Return the bean type that was being bound. + * @return the bean type + */ + public Class getBeanType() { + return this.beanType; + } + + /** + * Return the configuration properties annotation that triggered the binding. + * @return the configuration properties annotation + */ + public ConfigurationProperties getAnnotation() { + return this.annotation; + } + + private static String getMessage(Object bean, ConfigurationProperties annotation) { + StringBuilder message = new StringBuilder(); + message.append("Could not bind properties to '" + + ClassUtils.getShortName(bean.getClass()) + "' : "); + message.append("prefix=").append(annotation.prefix()); + message.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields()); + message.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields()); + return message.toString(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index 8484e31ae7..b1bfd14ccf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -16,6 +16,9 @@ package org.springframework.boot.context.properties; +import java.util.ArrayList; +import java.util.List; + import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; @@ -23,86 +26,78 @@ import org.springframework.boot.context.properties.bind.PropertySourcesPlacehold import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler; import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler; -import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter; -import org.springframework.core.ResolvableType; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.env.PropertySource; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.PropertySources; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; /** - * Bind {@link ConfigurationProperties} annotated object from a configurable list of - * {@link PropertySource}. + * Internal class by the {@link ConfigurationPropertiesBindingPostProcessor} to handle the + * actual {@link ConfigurationProperties} binding. * * @author Stephane Nicoll + * @author Phillip Webb */ class ConfigurationPropertiesBinder { - private final Iterable> propertySources; + private final ApplicationContext applicationContext; - private final ConversionService conversionService; + private final PropertySources propertySources; - private final Validator validator; + private final Validator configurationPropertiesValidator; - private Iterable configurationSources; + private final Validator jsr303Validator; - private final Binder binder; - - ConfigurationPropertiesBinder(Iterable> propertySources, - ConversionService conversionService, Validator validator) { - Assert.notNull(propertySources, "PropertySources must not be null"); - this.propertySources = propertySources; - this.conversionService = conversionService; - this.validator = validator; - this.configurationSources = ConfigurationPropertySources.from(propertySources); - this.binder = new Binder(this.configurationSources, - new PropertySourcesPlaceholdersResolver(this.propertySources), - this.conversionService); + private volatile Binder binder; + ConfigurationPropertiesBinder(ApplicationContext applicationContext, + String validatorBeanName) { + this.applicationContext = applicationContext; + this.propertySources = new PropertySourcesDeducer(applicationContext) + .getPropertySources(); + this.configurationPropertiesValidator = getConfigurationPropertiesValidator( + applicationContext, validatorBeanName); + this.jsr303Validator = ConfigurationPropertiesJsr303Validator + .getIfJsr303Present(applicationContext); } - /** - * Bind the specified {@code target} object using the configuration defined by the - * specified {@code annotation}. - * @param target the target to bind the configuration property sources to - * @param annotation the binding configuration - * @param targetType the resolvable type for the target - * @throws ConfigurationPropertiesBindingException if the binding failed - */ - void bind(Object target, ConfigurationProperties annotation, - ResolvableType targetType) { - Validator validator = determineValidator(target); - BindHandler handler = getBindHandler(annotation, validator); - Bindable bindable = Bindable.of(targetType).withExistingValue(target); - try { - this.binder.bind(annotation.prefix(), bindable, handler); - } - catch (Exception ex) { - String message = "Could not bind properties to '" - + ClassUtils.getShortName(target.getClass()) + "': " - + getAnnotationDetails(annotation); - throw new ConfigurationPropertiesBindingException(message, ex); - } + public void bind(Bindable target) { + ConfigurationProperties annotation = target + .getAnnotation(ConfigurationProperties.class); + Assert.state(annotation != null, "Missing @ConfigurationProperties on " + target); + List validators = getValidators(target); + BindHandler bindHandler = getBindHandler(annotation, validators); + getBinder().bind(annotation.prefix(), target, bindHandler); } - private Validator determineValidator(Object bean) { - boolean supportsBean = (this.validator != null - && this.validator.supports(bean.getClass())); - if (ClassUtils.isAssignable(Validator.class, bean.getClass())) { - if (supportsBean) { - return new ChainingValidator(this.validator, (Validator) bean); - } - return (Validator) bean; + private Validator getConfigurationPropertiesValidator( + ApplicationContext applicationContext, String validatorBeanName) { + if (applicationContext.containsBean(validatorBeanName)) { + return applicationContext.getBean(validatorBeanName, Validator.class); } - return (supportsBean ? this.validator : null); + return null; + } + + private List getValidators(Bindable target) { + List validators = new ArrayList<>(3); + if (this.configurationPropertiesValidator != null) { + validators.add(this.configurationPropertiesValidator); + } + if (this.jsr303Validator != null + && target.getAnnotation(Validated.class) != null) { + validators.add(this.jsr303Validator); + } + if (target.getValue() != null && target.getValue().get() instanceof Validator) { + validators.add((Validator) target.getValue().get()); + } + return validators; } private BindHandler getBindHandler(ConfigurationProperties annotation, - Validator validator) { + List validators) { BindHandler handler = BindHandler.DEFAULT; if (annotation.ignoreInvalidFields()) { handler = new IgnoreErrorsBindHandler(handler); @@ -111,55 +106,22 @@ class ConfigurationPropertiesBinder { UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter(); handler = new NoUnboundElementsBindHandler(handler, filter); } - if (validator != null) { - handler = new ValidationBindHandler(handler, validator); + if (!validators.isEmpty()) { + handler = new ValidationBindHandler(handler, + validators.toArray(new Validator[validators.size()])); } return handler; } - private String getAnnotationDetails(ConfigurationProperties annotation) { - if (annotation == null) { - return ""; + private Binder getBinder() { + if (this.binder == null) { + this.binder = new Binder( + ConfigurationPropertySources.from(this.propertySources), + new PropertySourcesPlaceholdersResolver(this.propertySources), + new ConversionServiceDeducer(this.applicationContext) + .getConversionService()); } - StringBuilder details = new StringBuilder(); - details.append("prefix=").append(annotation.prefix()); - details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields()); - details.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields()); - return details.toString(); - } - - /** - * {@link Validator} implementation that wraps {@link Validator} instances and chains - * their execution. - */ - private static class ChainingValidator implements Validator { - - private final Validator[] validators; - - ChainingValidator(Validator... validators) { - Assert.notNull(validators, "Validators must not be null"); - this.validators = validators; - } - - @Override - public boolean supports(Class clazz) { - for (Validator validator : this.validators) { - if (validator.supports(clazz)) { - return true; - } - } - return false; - } - - @Override - public void validate(Object target, Errors errors) { - for (Validator validator : this.validators) { - if (validator.supports(target.getClass())) { - validator.validate(target, errors); - } - } - } - + return this.binder; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderBuilder.java deleted file mode 100644 index b3bed831f8..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderBuilder.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2012-2018 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.List; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.env.PropertySource; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.validation.Validator; - -/** - * Builder for creating {@link ConfigurationPropertiesBinder} based on the state of the - * {@link ApplicationContext}. - * - * @author Stephane Nicoll - */ -class ConfigurationPropertiesBinderBuilder { - - /** - * The bean name of the configuration properties validator. - */ - static final String VALIDATOR_BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.VALIDATOR_BEAN_NAME; - - /** - * The bean name of the configuration properties conversion service. - */ - static final String CONVERSION_SERVICE_BEAN_NAME = ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME; - - private static final String[] VALIDATOR_CLASSES = { "javax.validation.Validator", - "javax.validation.ValidatorFactory", - "javax.validation.bootstrap.GenericBootstrap" }; - - private final ApplicationContext applicationContext; - - private ConversionService conversionService; - - private Validator validator; - - private Iterable> propertySources; - - /** - * Creates an instance with the {@link ApplicationContext} to use. - * @param applicationContext the application context - */ - ConfigurationPropertiesBinderBuilder(ApplicationContext applicationContext) { - Assert.notNull(applicationContext, "ApplicationContext must not be null"); - this.applicationContext = applicationContext; - } - - /** - * Specify the {@link PropertySource property sources} to use. - * @param propertySources the configuration the binder should use - * @return this instance - */ - ConfigurationPropertiesBinderBuilder withPropertySources( - Iterable> propertySources) { - this.propertySources = propertySources; - return this; - } - - /** - * Build a {@link ConfigurationPropertiesBinder} based on the state of the builder, - * discovering the {@link ConversionService} and {@link Validator} if necessary. - * @return a {@link ConfigurationPropertiesBinder} - */ - ConfigurationPropertiesBinder build() { - return new ConfigurationPropertiesBinder(this.propertySources, - determineConversionService(), determineValidator()); - } - - private Validator determineValidator() { - if (this.validator != null) { - return this.validator; - } - Validator defaultValidator = getOptionalBean(VALIDATOR_BEAN_NAME, - Validator.class); - if (defaultValidator != null) { - return defaultValidator; - } - if (isJsr303Present()) { - return new Jsr303ConfigurationPropertiesValidator(this.applicationContext); - } - return null; - } - - private ConversionService determineConversionService() { - if (this.conversionService != null) { - return this.conversionService; - } - ConversionService conversionServiceByName = getOptionalBean( - CONVERSION_SERVICE_BEAN_NAME, ConversionService.class); - if (conversionServiceByName != null) { - return conversionServiceByName; - } - return createDefaultConversionService(); - } - - private ConversionService createDefaultConversionService() { - ConversionServiceFactory conversionServiceFactory = this.applicationContext - .getAutowireCapableBeanFactory() - .createBean(ConversionServiceFactory.class); - return conversionServiceFactory.createConversionService(); - } - - private boolean isJsr303Present() { - for (String validatorClass : VALIDATOR_CLASSES) { - if (!ClassUtils.isPresent(validatorClass, - this.applicationContext.getClassLoader())) { - return false; - } - } - return true; - } - - private T getOptionalBean(String name, Class type) { - try { - return this.applicationContext.getBean(name, type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } - } - - private static class ConversionServiceFactory { - - private List> converters = Collections.emptyList(); - - private List genericConverters = Collections.emptyList(); - - /** - * A list of custom converters (in addition to the defaults) to use when - * converting properties for binding. - * @param converters the converters to set - */ - @Autowired(required = false) - @ConfigurationPropertiesBinding - public void setConverters(List> converters) { - this.converters = converters; - } - - /** - * A list of custom converters (in addition to the defaults) to use when - * converting properties for binding. - * @param converters the converters to set - */ - @Autowired(required = false) - @ConfigurationPropertiesBinding - public void setGenericConverters(List converters) { - this.genericConverters = converters; - } - - public ConversionService createConversionService() { - DefaultConversionService conversionService = new DefaultConversionService(); - for (Converter converter : this.converters) { - conversionService.addConverter(converter); - } - for (GenericConverter genericConverter : this.genericConverters) { - conversionService.addConverter(genericConverter); - } - return conversionService; - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingException.java deleted file mode 100644 index b5d02dc390..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingException.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 org.springframework.core.NestedExceptionUtils; - -/** - * Exception thrown when a {@code @ConfigurationProperties} annotated object failed to be - * bound. - * - * @author Stephane Nicoll - */ -class ConfigurationPropertiesBindingException extends RuntimeException { - - ConfigurationPropertiesBindingException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Retrieve the innermost cause of this exception, if any. - * @return the innermost exception, or {@code null} if none - */ - public Throwable getRootCause() { - return NestedExceptionUtils.getRootCause(this); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index 458cada31c..c62621b486 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -16,32 +16,21 @@ package org.springframework.boot.context.properties; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.context.EnvironmentAware; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySources; -import org.springframework.core.env.StandardEnvironment; +import org.springframework.validation.annotation.Validated; /** * {@link BeanPostProcessor} to bind {@link PropertySources} to beans annotated with @@ -53,156 +42,90 @@ import org.springframework.core.env.StandardEnvironment; * @author Stephane Nicoll * @author Madhura Bhave */ -public class ConfigurationPropertiesBindingPostProcessor - implements BeanPostProcessor, BeanFactoryAware, EnvironmentAware, - ApplicationContextAware, InitializingBean, PriorityOrdered { +public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, + PriorityOrdered, ApplicationContextAware, InitializingBean { + + /** + * The bean name that this post-processor is registered with. + */ + public static final String BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class + .getName(); /** * The bean name of the configuration properties validator. */ public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator"; - private static final Log logger = LogFactory - .getLog(ConfigurationPropertiesBindingPostProcessor.class); - - private ConfigurationBeanFactoryMetadata beans = new ConfigurationBeanFactoryMetadata(); - - private BeanFactory beanFactory; - - private Environment environment = new StandardEnvironment(); + private ConfigurationBeanFactoryMetadata beanFactoryMetadata; private ApplicationContext applicationContext; private ConfigurationPropertiesBinder configurationPropertiesBinder; - private PropertySources propertySources; - - /** - * Return the order of the bean. - * @return the order - */ @Override - public int getOrder() { - return Ordered.HIGHEST_PRECEDENCE + 1; - } - - /** - * Set the bean meta-data store. - * @param beans the bean meta data store - */ - public void setBeanMetadataStore(ConfigurationBeanFactoryMetadata beans) { - this.beans = beans; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) { + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { this.applicationContext = applicationContext; } @Override public void afterPropertiesSet() throws Exception { - this.propertySources = deducePropertySources(); + // We can't use constructor injection of the application context because + // it causes eager factory bean initialization + this.beanFactoryMetadata = this.applicationContext.getBean( + ConfigurationBeanFactoryMetadata.BEAN_NAME, + ConfigurationBeanFactoryMetadata.class); + this.configurationPropertiesBinder = new ConfigurationPropertiesBinder( + this.applicationContext, VALIDATOR_BEAN_NAME); } - private PropertySources deducePropertySources() { - MutablePropertySources environmentPropertySources = extractEnvironmentPropertySources(); - PropertySourcesPlaceholderConfigurer configurer = getSinglePropertySourcesPlaceholderConfigurer(); - if (configurer == null) { - if (environmentPropertySources != null) { - return environmentPropertySources; - } - throw new IllegalStateException("Unable to obtain PropertySources from " - + "PropertySourcesPlaceholderConfigurer or Environment"); - } - PropertySources appliedPropertySources = configurer.getAppliedPropertySources(); - if (environmentPropertySources == null) { - return appliedPropertySources; - } - return new CompositePropertySources(new FilteredPropertySources( - appliedPropertySources, - PropertySourcesPlaceholderConfigurer.ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME), - environmentPropertySources); - } - - private MutablePropertySources extractEnvironmentPropertySources() { - if (this.environment instanceof ConfigurableEnvironment) { - return ((ConfigurableEnvironment) this.environment).getPropertySources(); - } - return null; - } - - private PropertySourcesPlaceholderConfigurer getSinglePropertySourcesPlaceholderConfigurer() { - // Take care not to cause early instantiation of all FactoryBeans - if (this.beanFactory instanceof ListableBeanFactory) { - ListableBeanFactory listableBeanFactory = (ListableBeanFactory) this.beanFactory; - Map beans = listableBeanFactory - .getBeansOfType(PropertySourcesPlaceholderConfigurer.class, false, - false); - if (beans.size() == 1) { - return beans.values().iterator().next(); - } - if (beans.size() > 1 && logger.isWarnEnabled()) { - logger.warn("Multiple PropertySourcesPlaceholderConfigurer " - + "beans registered " + beans.keySet() - + ", falling back to Environment"); - } - } - return null; + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 1; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - ConfigurationProperties annotation = getAnnotation(bean, beanName); + ConfigurationProperties annotation = getAnnotation(bean, beanName, + ConfigurationProperties.class); if (annotation != null) { - try { - ResolvableType type = ResolvableType.forClass(bean.getClass()); - Method factoryMethod = this.beans.findFactoryMethod(beanName); - if (factoryMethod != null) { - type = ResolvableType.forMethodReturnType(factoryMethod); - } - getBinder().bind(bean, annotation, type); - } - catch (ConfigurationPropertiesBindingException ex) { - throw new BeanCreationException(beanName, ex.getMessage(), ex.getCause()); - } + bind(bean, beanName, annotation); } return bean; } - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) - throws BeansException { - return bean; + private void bind(Object bean, String beanName, ConfigurationProperties annotation) { + ResolvableType type = getBeanType(bean, beanName); + Validated validated = getAnnotation(bean, beanName, Validated.class); + Annotation[] annotations = (validated == null ? new Annotation[] { annotation } + : new Annotation[] { annotation, validated }); + Bindable target = Bindable.of(type).withExistingValue(bean) + .withAnnotations(annotations); + try { + this.configurationPropertiesBinder.bind(target); + } + catch (Exception ex) { + throw new ConfigurationPropertiesBindException(beanName, bean, annotation, + ex); + } } - private ConfigurationProperties getAnnotation(Object bean, String beanName) { - ConfigurationProperties annotation = this.beans.findFactoryAnnotation(beanName, - ConfigurationProperties.class); + private ResolvableType getBeanType(Object bean, String beanName) { + Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName); + if (factoryMethod != null) { + return ResolvableType.forMethodReturnType(factoryMethod); + } + return ResolvableType.forClass(bean.getClass()); + } + + private A getAnnotation(Object bean, String beanName, + Class type) { + A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type); if (annotation == null) { - annotation = AnnotationUtils.findAnnotation(bean.getClass(), - ConfigurationProperties.class); + annotation = AnnotationUtils.findAnnotation(bean.getClass(), type); } return annotation; } - private ConfigurationPropertiesBinder getBinder() { - if (this.configurationPropertiesBinder == null) { - this.configurationPropertiesBinder = new ConfigurationPropertiesBinderBuilder( - this.applicationContext).withPropertySources(this.propertySources) - .build(); - } - return this.configurationPropertiesBinder; - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java index 0446ce91fc..d876563504 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorRegistrar.java @@ -16,8 +16,9 @@ package org.springframework.boot.context.properties; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; @@ -31,26 +32,33 @@ import org.springframework.core.type.AnnotationMetadata; public class ConfigurationPropertiesBindingPostProcessorRegistrar implements ImportBeanDefinitionRegistrar { - /** - * The bean name of the {@link ConfigurationPropertiesBindingPostProcessor}. - */ - public static final String BINDER_BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class - .getName(); - - private static final String METADATA_BEAN_NAME = BINDER_BEAN_NAME + ".store"; - @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - if (!registry.containsBeanDefinition(BINDER_BEAN_NAME)) { - BeanDefinitionBuilder meta = BeanDefinitionBuilder - .genericBeanDefinition(ConfigurationBeanFactoryMetadata.class); - BeanDefinitionBuilder bean = BeanDefinitionBuilder.genericBeanDefinition( - ConfigurationPropertiesBindingPostProcessor.class); - bean.addPropertyReference("beanMetadataStore", METADATA_BEAN_NAME); - registry.registerBeanDefinition(BINDER_BEAN_NAME, bean.getBeanDefinition()); - registry.registerBeanDefinition(METADATA_BEAN_NAME, meta.getBeanDefinition()); + if (!registry.containsBeanDefinition( + ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) { + registerConfigurationPropertiesBindingPostProcessor(registry); + registerConfigurationBeanFactoryMetadata(registry); } } + private void registerConfigurationPropertiesBindingPostProcessor( + BeanDefinitionRegistry registry) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition( + ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition); + + } + + private void registerConfigurationBeanFactoryMetadata( + BeanDefinitionRegistry registry) { + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(ConfigurationBeanFactoryMetadata.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME, + definition); + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/Jsr303ConfigurationPropertiesValidator.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesJsr303Validator.java similarity index 67% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/Jsr303ConfigurationPropertiesValidator.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesJsr303Validator.java index 0cbc58a1a7..3c8cf0bfdf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/Jsr303ConfigurationPropertiesValidator.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesJsr303Validator.java @@ -18,7 +18,7 @@ package org.springframework.boot.context.properties; import org.springframework.boot.validation.MessageInterpolatorFactory; import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; @@ -30,18 +30,22 @@ import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; * * @author Phillip Webb */ -class Jsr303ConfigurationPropertiesValidator implements Validator { +final class ConfigurationPropertiesJsr303Validator implements Validator { + + private static final String[] VALIDATOR_CLASSES = { "javax.validation.Validator", + "javax.validation.ValidatorFactory", + "javax.validation.bootstrap.GenericBootstrap" }; private final Delegate delegate; - Jsr303ConfigurationPropertiesValidator(ApplicationContext applicationContext) { + private ConfigurationPropertiesJsr303Validator( + ApplicationContext applicationContext) { this.delegate = new Delegate(applicationContext); } @Override public boolean supports(Class type) { - return AnnotatedElementUtils.hasAnnotation(type, Validated.class) - && this.delegate.supports(type); + return this.delegate.supports(type); } @Override @@ -49,6 +53,17 @@ class Jsr303ConfigurationPropertiesValidator implements Validator { this.delegate.validate(target, errors); } + public static ConfigurationPropertiesJsr303Validator getIfJsr303Present( + ApplicationContext applicationContext) { + ClassLoader classLoader = applicationContext.getClassLoader(); + for (String validatorClass : VALIDATOR_CLASSES) { + if (!ClassUtils.isPresent(validatorClass, classLoader)) { + return null; + } + } + return new ConfigurationPropertiesJsr303Validator(applicationContext); + } + private static class Delegate extends LocalValidatorFactoryBean { Delegate(ApplicationContext applicationContext) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java new file mode 100644 index 0000000000..52cce891e7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2018 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.List; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.convert.support.DefaultConversionService; + +/** + * Utility to deduce the {@link ConversionService} to use for configuration properties + * binding. + * + * @author Phillip Webb + */ +class ConversionServiceDeducer { + + private final ApplicationContext applicationContext; + + ConversionServiceDeducer(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public ConversionService getConversionService() { + try { + return this.applicationContext.getBean( + ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME, + ConversionService.class); + } + catch (NoSuchBeanDefinitionException ex) { + return this.applicationContext.getAutowireCapableBeanFactory() + .createBean(Factory.class).create(); + } + } + + private static class Factory { + + private List> converters = Collections.emptyList(); + + private List genericConverters = Collections.emptyList(); + + /** + * A list of custom converters (in addition to the defaults) to use when + * converting properties for binding. + * @param converters the converters to set + */ + @Autowired(required = false) + @ConfigurationPropertiesBinding + public void setConverters(List> converters) { + this.converters = converters; + } + + /** + * A list of custom converters (in addition to the defaults) to use when + * converting properties for binding. + * @param converters the converters to set + */ + @Autowired(required = false) + @ConfigurationPropertiesBinding + public void setGenericConverters(List converters) { + this.genericConverters = converters; + } + + public ConversionService create() { + DefaultConversionService conversionService = new DefaultConversionService(); + for (Converter converter : this.converters) { + conversionService.addConverter(converter); + } + for (GenericConverter genericConverter : this.genericConverters) { + conversionService.addConverter(genericConverter); + } + return conversionService; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java index 12e0f1a996..ff39b73d32 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -16,13 +16,15 @@ package org.springframework.boot.context.properties; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.annotation.AnnotationUtils; @@ -47,19 +49,13 @@ import org.springframework.util.StringUtils; */ class EnableConfigurationPropertiesImportSelector implements ImportSelector { + private static final String[] IMPORTS = { + ConfigurationPropertiesBeanRegistrar.class.getName(), + ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() }; + @Override public String[] selectImports(AnnotationMetadata metadata) { - MultiValueMap attributes = metadata.getAllAnnotationAttributes( - EnableConfigurationProperties.class.getName(), false); - Object[] type = attributes == null ? null - : (Object[]) attributes.getFirst("value"); - if (type == null || type.length == 0) { - return new String[] { - ConfigurationPropertiesBindingPostProcessorRegistrar.class - .getName() }; - } - return new String[] { ConfigurationPropertiesBeanRegistrar.class.getName(), - ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() }; + return IMPORTS; } /** @@ -71,73 +67,68 @@ class EnableConfigurationPropertiesImportSelector implements ImportSelector { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + getTypes(metadata).forEach((type) -> register(registry, + (ConfigurableListableBeanFactory) registry, type)); + } + + private List> getTypes(AnnotationMetadata metadata) { MultiValueMap attributes = metadata .getAllAnnotationAttributes( EnableConfigurationProperties.class.getName(), false); - List> types = collectClasses(attributes.get("value")); - for (Class type : types) { - String prefix = extractPrefix(type); - String name = (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() - : type.getName()); - if (!containsBeanDefinition((ConfigurableListableBeanFactory) registry, - name)) { - registerBeanDefinition(registry, type, name); - } + return collectClasses(attributes == null ? Collections.emptyList() + : attributes.get("value")); + } + + private List> collectClasses(List values) { + return values.stream().flatMap((value) -> Arrays.stream((Object[]) value)) + .map((o) -> (Class) o).filter((type) -> void.class != type) + .collect(Collectors.toList()); + } + + private void register(BeanDefinitionRegistry registry, + ConfigurableListableBeanFactory beanFactory, Class type) { + String name = getName(type); + if (!containsBeanDefinition(beanFactory, name)) { + registerBeanDefinition(registry, name, type); } } - private String extractPrefix(Class type) { + private String getName(Class type) { ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type, ConfigurationProperties.class); - if (annotation != null) { - return annotation.prefix(); - } - return ""; - } - - private List> collectClasses(List list) { - ArrayList> result = new ArrayList<>(); - for (Object object : list) { - for (Object value : (Object[]) object) { - if (value instanceof Class && value != void.class) { - result.add((Class) value); - } - } - } - return result; - } - - private void registerBeanDefinition(BeanDefinitionRegistry registry, - Class type, String name) { - BeanDefinitionBuilder builder = BeanDefinitionBuilder - .genericBeanDefinition(type); - AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); - registry.registerBeanDefinition(name, beanDefinition); - - ConfigurationProperties properties = AnnotationUtils.findAnnotation(type, - ConfigurationProperties.class); - Assert.notNull(properties, - "No " + ConfigurationProperties.class.getSimpleName() - + " annotation found on '" + type.getName() + "'."); + String prefix = (annotation != null ? annotation.prefix() : ""); + return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() + : type.getName()); } private boolean containsBeanDefinition( ConfigurableListableBeanFactory beanFactory, String name) { - - boolean result = beanFactory.containsBeanDefinition(name); - if (result) { + if (beanFactory.containsBeanDefinition(name)) { return true; } - if (beanFactory - .getParentBeanFactory() instanceof ConfigurableListableBeanFactory) { - return containsBeanDefinition( - (ConfigurableListableBeanFactory) beanFactory - .getParentBeanFactory(), + BeanFactory parent = beanFactory.getParentBeanFactory(); + if (parent instanceof ConfigurableListableBeanFactory) { + return containsBeanDefinition((ConfigurableListableBeanFactory) parent, name); } return false; } + private void registerBeanDefinition(BeanDefinitionRegistry registry, String name, + Class type) { + assertHasAnnotation(type); + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(type); + registry.registerBeanDefinition(name, definition); + } + + private void assertHasAnnotation(Class type) { + Assert.notNull( + AnnotationUtils.findAnnotation(type, ConfigurationProperties.class), + "No " + ConfigurationProperties.class.getSimpleName() + + " annotation found on '" + type.getName() + "'."); + } + } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertySourcesDeducer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertySourcesDeducer.java new file mode 100644 index 0000000000..aea197253e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertySourcesDeducer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2018 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.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySources; +import org.springframework.util.Assert; + +/** + * Utility to deduce the {@link PropertySources} to use for configuration binding. + * + * @author Phillip Webb + */ +class PropertySourcesDeducer { + + private static final Log logger = LogFactory.getLog(PropertySourcesDeducer.class); + + private final ApplicationContext applicationContext; + + PropertySourcesDeducer(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public PropertySources getPropertySources() { + MutablePropertySources environmentPropertySources = extractEnvironmentPropertySources(); + PropertySourcesPlaceholderConfigurer placeholderConfigurer = getSinglePropertySourcesPlaceholderConfigurer(); + if (placeholderConfigurer == null) { + Assert.state(environmentPropertySources != null, + "Unable to obtain PropertySources from " + + "PropertySourcesPlaceholderConfigurer or Environment"); + return environmentPropertySources; + } + PropertySources appliedPropertySources = placeholderConfigurer + .getAppliedPropertySources(); + if (environmentPropertySources == null) { + return appliedPropertySources; + } + return merge(environmentPropertySources, appliedPropertySources); + } + + private MutablePropertySources extractEnvironmentPropertySources() { + Environment environment = this.applicationContext.getEnvironment(); + if (environment instanceof ConfigurableEnvironment) { + return ((ConfigurableEnvironment) environment).getPropertySources(); + } + return null; + } + + private PropertySourcesPlaceholderConfigurer getSinglePropertySourcesPlaceholderConfigurer() { + // Take care not to cause early instantiation of all FactoryBeans + Map beans = this.applicationContext + .getBeansOfType(PropertySourcesPlaceholderConfigurer.class, false, false); + if (beans.size() == 1) { + return beans.values().iterator().next(); + } + if (beans.size() > 1 && logger.isWarnEnabled()) { + logger.warn( + "Multiple PropertySourcesPlaceholderConfigurer " + "beans registered " + + beans.keySet() + ", falling back to Environment"); + } + return null; + } + + private PropertySources merge(PropertySources environmentPropertySources, + PropertySources appliedPropertySources) { + FilteredPropertySources filtered = new FilteredPropertySources( + appliedPropertySources, + PropertySourcesPlaceholderConfigurer.ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME); + return new CompositePropertySources(filtered, environmentPropertySources); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java index 6bb4af413a..57cfe3f18d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -86,6 +86,22 @@ public final class Bindable { return this.annotations; } + /** + * Return a single associated annotations that could affect binding. + * @param the annotation type + * @param type annotation type + * @return the associated annotation or {@code null} + */ + @SuppressWarnings("unchecked") + public A getAnnotation(Class type) { + for (Annotation annotation : this.annotations) { + if (type.isInstance(annotation)) { + return (A) annotation; + } + } + return null; + } + @Override public String toString() { ToStringCreator creator = new ToStringCreator(this); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java index 8ff51dc400..10b2250701 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/BindValidationException.java @@ -32,6 +32,7 @@ public class BindValidationException extends RuntimeException { private final ValidationErrors validationErrors; BindValidationException(ValidationErrors validationErrors) { + super(getMessage(validationErrors)); Assert.notNull(validationErrors, "ValidationErrors must not be null"); this.validationErrors = validationErrors; } @@ -44,4 +45,14 @@ public class BindValidationException extends RuntimeException { return this.validationErrors; } + private static String getMessage(ValidationErrors errors) { + StringBuilder message = new StringBuilder("Binding validation errors"); + if (errors != null) { + message.append(" on " + errors.getName()); + errors.getAllErrors().forEach( + (error) -> message.append(String.format("%n - %s", error))); + } + return message.toString(); + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java index bd4b0be943..ba98387f12 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -17,7 +17,9 @@ package org.springframework.boot.context.properties.bind.validation; import java.util.Arrays; +import java.util.Deque; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.Set; import java.util.stream.Collectors; @@ -27,11 +29,9 @@ import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; /** * {@link BindHandler} to apply {@link Validator Validators} to bound results. @@ -44,10 +44,10 @@ public class ValidationBindHandler extends AbstractBindHandler { private final Validator[] validators; - private boolean validate; - private final Set boundProperties = new LinkedHashSet<>(); + private final Deque exceptions = new LinkedList<>(); + public ValidationBindHandler(Validator... validators) { this.validators = validators; } @@ -57,21 +57,6 @@ public class ValidationBindHandler extends AbstractBindHandler { this.validators = validators; } - @Override - public boolean onStart(ConfigurationPropertyName name, Bindable target, - BindContext context) { - if (context.getDepth() == 0) { - this.validate = shouldValidate(target); - } - return super.onStart(name, target, context); - } - - private boolean shouldValidate(Bindable target) { - Validated annotation = AnnotationUtils - .findAnnotation(target.getBoxedType().resolve(), Validated.class); - return (annotation != null); - } - @Override public Object onSuccess(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { @@ -84,8 +69,9 @@ public class ValidationBindHandler extends AbstractBindHandler { @Override public void onFinish(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) throws Exception { - if (this.validate) { - validate(name, target, result); + validate(name, target, result); + if (context.getDepth() == 0 && !this.exceptions.isEmpty()) { + throw this.exceptions.pop(); } super.onFinish(name, target, context, result); } @@ -110,22 +96,22 @@ public class ValidationBindHandler extends AbstractBindHandler { private void validate(ConfigurationPropertyName name, Object target, Class type) { if (target != null) { BindingResult errors = new BeanPropertyBindingResult(target, name.toString()); - Arrays.stream(this.validators).filter((v) -> v.supports(type)) - .forEach((v) -> v.validate(target, errors)); + Arrays.stream(this.validators).filter((validator) -> validator.supports(type)) + .forEach((validator) -> validator.validate(target, errors)); if (errors.hasErrors()) { - throwBindValidationException(name, errors); + this.exceptions.push(getBindValidationException(name, errors)); } } } - private void throwBindValidationException(ConfigurationPropertyName name, - BindingResult errors) { + private BindValidationException getBindValidationException( + ConfigurationPropertyName name, BindingResult errors) { Set boundProperties = this.boundProperties.stream() .filter((property) -> name.isAncestorOf(property.getName())) .collect(Collectors.toCollection(LinkedHashSet::new)); ValidationErrors validationErrors = new ValidationErrors(name, boundProperties, errors.getAllErrors()); - throw new BindValidationException(validationErrors); + return new BindValidationException(validationErrors); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java index 39a996eba5..5f595cf103 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationErrors.java @@ -22,11 +22,11 @@ import java.util.Iterator; import java.util.List; import java.util.Set; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; import org.springframework.util.Assert; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; @@ -76,7 +76,7 @@ public class ValidationErrors implements Iterable { private FieldError convertFieldError(ConfigurationPropertyName name, Set boundProperties, FieldError error) { - if (error instanceof ObjectProvider) { + if (error instanceof OriginProvider) { return error; } return OriginTrackedFieldError.of(error, diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderBuilderTests.java deleted file mode 100644 index aed5ca8388..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderBuilderTests.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2012-2018 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.time.Duration; - -import javax.validation.constraints.NotNull; - -import org.junit.Test; - -import org.springframework.boot.context.properties.bind.validation.BindValidationException; -import org.springframework.boot.context.properties.bind.validation.ValidationErrors; -import org.springframework.context.support.StaticApplicationContext; -import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.convert.support.DefaultConversionService; -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; - -/** - * Tests for {@link ConfigurationPropertiesBinderBuilder}. - * - * @author Stephane Nicoll - */ -public class ConfigurationPropertiesBinderBuilderTests { - - private final StaticApplicationContext applicationContext = new StaticApplicationContext(); - - private final ConfigurationPropertiesBinderBuilder builder = new ConfigurationPropertiesBinderBuilder( - this.applicationContext); - - private final MockEnvironment environment = new MockEnvironment(); - - @Test - public void detectDefaultConversionService() { - this.applicationContext.registerSingleton("conversionService", - DefaultConversionService.class); - ConfigurationPropertiesBinder binder = builderWithSources().build(); - assertThat(ReflectionTestUtils.getField(binder, "conversionService")) - .isSameAs(this.applicationContext.getBean("conversionService")); - } - - @Test - public void bindToJavaTimeDuration() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "test.duration=PT1M"); - ConfigurationPropertiesBinder binder = builderWithSources().build(); - PropertyWithDuration target = new PropertyWithDuration(); - bind(binder, target); - assertThat(target.getDuration().getSeconds()).isEqualTo(60); - } - - @Test - public void detectDefaultValidator() { - this.applicationContext.registerSingleton( - ConfigurationPropertiesBindingPostProcessor.VALIDATOR_BEAN_NAME, - LocalValidatorFactoryBean.class); - ConfigurationPropertiesBinder binder = builderWithSources().build(); - assertThat(ReflectionTestUtils.getField(binder, "validator")) - .isSameAs(this.applicationContext.getBean( - ConfigurationPropertiesBindingPostProcessor.VALIDATOR_BEAN_NAME)); - } - - @Test - public void validationWithoutJsr303() { - ConfigurationPropertiesBinder binder = builderWithSources().build(); - assertThat(bindWithValidationErrors(binder, new PropertyWithoutJSR303()) - .getAllErrors()).hasSize(1); - } - - @Test - public void validationWithJsr303() { - ConfigurationPropertiesBinder binder = builderWithSources().build(); - assertThat( - bindWithValidationErrors(binder, new PropertyWithJSR303()).getAllErrors()) - .hasSize(2); - } - - private ConfigurationPropertiesBinderBuilder builderWithSources() { - return this.builder.withPropertySources(this.environment.getPropertySources()); - } - - @Test - public void validationWithJsr303AndValidInput() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "test.foo=123456", "test.bar=654321"); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertyWithJSR303 target = new PropertyWithJSR303(); - bind(binder, target); - assertThat(target.getFoo()).isEqualTo("123456"); - assertThat(target.getBar()).isEqualTo("654321"); - } - - private ValidationErrors bindWithValidationErrors( - ConfigurationPropertiesBinder binder, Object target) { - try { - bind(binder, target); - throw new AssertionError("Should have failed to bind " + target); - } - catch (ConfigurationPropertiesBindingException ex) { - Throwable rootCause = ex.getRootCause(); - assertThat(rootCause).isInstanceOf(BindValidationException.class); - return ((BindValidationException) rootCause).getValidationErrors(); - } - } - - private void bind(ConfigurationPropertiesBinder binder, Object target) { - binder.bind(target, - AnnotationUtils.findAnnotation(target.getClass(), - ConfigurationProperties.class), - ResolvableType.forType(target.getClass())); - } - - @ConfigurationProperties(prefix = "test") - public static class PropertyWithDuration { - - private Duration duration; - - public Duration getDuration() { - return this.duration; - } - - public void setDuration(Duration duration) { - this.duration = duration; - } - - } - - @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; - } - - } - - @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; - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderTests.java deleted file mode 100644 index d6c401a75b..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinderTests.java +++ /dev/null @@ -1,485 +0,0 @@ -/* - * Copyright 2012-2018 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.List; -import java.util.Map; - -import org.junit.Test; - -import org.springframework.boot.context.properties.bind.BindException; -import org.springframework.boot.context.properties.bind.validation.BindValidationException; -import org.springframework.boot.context.properties.bind.validation.ValidationErrors; -import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.env.SystemEnvironmentPropertySource; -import org.springframework.mock.env.MockEnvironment; -import org.springframework.test.context.support.TestPropertySourceUtils; -import org.springframework.validation.Errors; -import org.springframework.validation.ValidationUtils; -import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link ConfigurationPropertiesBinder}. - * - * @author Stephane Nicoll - */ -public class ConfigurationPropertiesBinderTests { - - private final MockEnvironment environment = new MockEnvironment(); - - @Test - public void bindSimpleProperties() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "person.name=John Smith", "person.age=42"); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PersonProperties target = new PersonProperties(); - bind(binder, target); - assertThat(target.name).isEqualTo("John Smith"); - assertThat(target.age).isEqualTo(42); - } - - @Test - public void bindUnknownFieldFailureMessageContainsDetailsOfPropertyOrigin() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "person.does-not-exist=yolo"); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PersonProperties target = new PersonProperties(); - try { - bind(binder, target); - fail("Expected exception"); - } - catch (ConfigurationPropertiesBindingException ex) { - BindException bindException = (BindException) ex.getCause(); - assertThat(bindException.getMessage()) - .startsWith("Failed to bind properties under 'person' to " - + PersonProperties.class.getName()); - } - } - - @Test - public void bindWithIgnoreInvalidFieldsAnnotation() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "com.example.bar=spam"); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertyWithIgnoreInvalidFields target = new PropertyWithIgnoreInvalidFields(); - bind(binder, target); - assertThat(target.getBar()).isEqualTo(0); - } - - @Test - public void bindToEnum() { - bindToEnum("test.theValue=foo"); - } - - @Test - public void bindToEnumRelaxed() { - bindToEnum("test.the-value=FoO"); - bindToEnum("test.THE_VALUE=FoO"); - } - - private void bindToEnum(String property) { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - property); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertyWithEnum target = new PropertyWithEnum(); - bind(binder, target); - assertThat(target.getTheValue()).isEqualTo(FooEnum.FOO); - } - - @Test - public void bindSetOfEnumRelaxed() { - bindToEnumSet("test.the-values=foo,bar", FooEnum.FOO, FooEnum.BAR); - bindToEnumSet("test.the-values=foo", FooEnum.FOO); - } - - private void bindToEnumSet(String property, FooEnum... expected) { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - property); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertyWithEnum target = new PropertyWithEnum(); - bind(binder, target); - assertThat(target.getTheValues()).contains(expected); - } - - @Test - public void bindToCharArray() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "test.chars=word"); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertyWithCharArray target = new PropertyWithCharArray(); - bind(binder, target); - assertThat(target.getChars()).isEqualTo("word".toCharArray()); - } - - @Test - public void bindToRelaxedPropertyNamesSame() { - 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... pairs) { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - pairs); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertyWithRelaxedNames target = new PropertyWithRelaxedNames(); - bind(binder, target); - assertThat(target.getFooBar()).isEqualTo("test2"); - assertThat(target.getBarBAZ()).isEqualTo("testb"); - } - - @Test - public void bindToNestedProperty() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "test.nested.value=test1"); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertyWithNestedValue target = new PropertyWithNestedValue(); - bind(binder, target); - assertThat(target.getNested().getValue()).isEqualTo("test1"); - } - - @Test - public void bindToMap() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "test.map.foo=bar"); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertiesWithMap target = new PropertiesWithMap(); - bind(binder, target); - assertThat(target.getMap()).containsOnly(entry("foo", "bar")); - } - - @Test - public void bindToMapWithSystemProperties() { - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addLast(new SystemEnvironmentPropertySource( - StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, - Collections.singletonMap("TEST_MAP_FOO_BAR", "baz"))); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - propertySources, null, null); - PropertiesWithComplexMap target = new PropertiesWithComplexMap(); - bind(binder, target); - assertThat(target.getMap()).containsOnlyKeys("foo"); - assertThat(target.getMap().get("foo")).containsOnly(entry("bar", "baz")); - } - - @Test - public void bindWithOverriddenProperties() { - MutablePropertySources propertySources = new MutablePropertySources(); - propertySources.addFirst(new SystemEnvironmentPropertySource("system", - Collections.singletonMap("PERSON_NAME", "Jane"))); - propertySources.addLast(new MapPropertySource("test", - Collections.singletonMap("person.name", "John"))); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - propertySources, null, null); - PersonProperties target = new PersonProperties(); - bind(binder, target); - assertThat(target.name).isEqualTo("Jane"); - } - - @Test - public void validationWithSetter() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "test.foo=spam"); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, null); - PropertyWithValidatingSetter target = new PropertyWithValidatingSetter(); - try { - bind(binder, target); - fail("Expected exception"); - } - catch (ConfigurationPropertiesBindingException ex) { - BindException bindException = (BindException) ex.getCause(); - assertThat(bindException.getMessage()) - .startsWith("Failed to bind properties under 'test' to " - + PropertyWithValidatingSetter.class.getName()); - } - } - - @Test - public void validationWithCustomValidator() { - CustomPropertyValidator validator = spy(new CustomPropertyValidator()); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, validator); - PropertyWithCustomValidator target = new PropertyWithCustomValidator(); - assertThat(bindWithValidationErrors(binder, target).getAllErrors()).hasSize(1); - verify(validator).validate(eq(target), any(Errors.class)); - } - - @Test - public void validationWithCustomValidatorNotSupported() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, - "test.foo=bar"); - CustomPropertyValidator validator = spy(new CustomPropertyValidator()); - ConfigurationPropertiesBinder binder = new ConfigurationPropertiesBinder( - this.environment.getPropertySources(), null, validator); - PropertyWithValidatingSetter target = new PropertyWithValidatingSetter(); - bind(binder, target); - assertThat(target.getFoo()).isEqualTo("bar"); - verify(validator, times(0)).validate(eq(target), any(Errors.class)); - } - - private ValidationErrors bindWithValidationErrors( - ConfigurationPropertiesBinder binder, Object target) { - try { - bind(binder, target); - throw new AssertionError("Should have failed to bind " + target); - } - catch (ConfigurationPropertiesBindingException ex) { - Throwable rootCause = ex.getRootCause(); - assertThat(rootCause).isInstanceOf(BindValidationException.class); - return ((BindValidationException) rootCause).getValidationErrors(); - } - } - - private void bind(ConfigurationPropertiesBinder binder, Object target) { - binder.bind(target, - AnnotationUtils.findAnnotation(target.getClass(), - ConfigurationProperties.class), - ResolvableType.forType(target.getClass())); - } - - @ConfigurationProperties(value = "person", ignoreUnknownFields = false) - static class PersonProperties { - - private String name; - - private Integer age; - - public void setName(String name) { - this.name = name; - } - - public void setAge(Integer age) { - this.age = age; - } - - } - - @ConfigurationProperties(prefix = "com.example", ignoreInvalidFields = true) - public static class PropertyWithIgnoreInvalidFields { - - private long bar; - - public void setBar(long bar) { - this.bar = bar; - } - - public long getBar() { - return this.bar; - } - - } - - @ConfigurationProperties(prefix = "test") - public static class PropertyWithEnum { - - private FooEnum theValue; - - private List theValues; - - public void setTheValue(FooEnum value) { - this.theValue = value; - } - - public FooEnum getTheValue() { - return this.theValue; - } - - public List getTheValues() { - return this.theValues; - } - - public void setTheValues(List theValues) { - this.theValues = theValues; - } - - } - - enum FooEnum { - - FOO, BAZ, BAR - - } - - @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; - } - - } - - @ConfigurationProperties(prefix = "test") - public static class PropertyWithRelaxedNames { - - 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; - } - - } - - @ConfigurationProperties(prefix = "test") - public static class PropertyWithNestedValue { - - private Nested nested = new Nested(); - - public Nested getNested() { - return this.nested; - } - - public static class Nested { - - private String value; - - public void setValue(String value) { - this.value = value; - } - - public String getValue() { - return this.value; - } - - } - - } - - @Validated - @ConfigurationProperties(prefix = "test") - public static class PropertiesWithMap { - - private Map map; - - public Map getMap() { - return this.map; - } - - public void setMap(Map map) { - this.map = map; - } - - } - - @ConfigurationProperties(prefix = "test") - public static class PropertiesWithComplexMap { - - private Map> map; - - public Map> getMap() { - return this.map; - } - - public void setMap(Map> map) { - this.map = map; - } - - } - - @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"); - } - } - - } - - @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"); - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java deleted file mode 100644 index 3e32366f83..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java +++ /dev/null @@ -1,689 +0,0 @@ -/* - * Copyright 2012-2018 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.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -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.testsupport.rule.OutputCapture; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.env.SystemEnvironmentPropertySource; -import org.springframework.lang.Nullable; -import org.springframework.mock.env.MockEnvironment; -import org.springframework.test.context.support.TestPropertySourceUtils; -import org.springframework.util.StringUtils; -import org.springframework.validation.annotation.Validated; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.instanceOf; - -/** - * 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 OutputCapture output = new OutputCapture(); - - private AnnotationConfigApplicationContext context; - - @After - public void close() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - public void bindToInterfaceBean() { - 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 initializerSeeBoundProperties() { - 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 bindWithValueDefault() { - 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 binderShouldNotInitializeFactoryBeans() { - 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 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 multiplePropertySourcesPlaceholderConfigurer() { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(MultiplePropertySourcesPlaceholderConfigurer.class); - this.context.refresh(); - assertThat(this.output.toString()).contains( - "Multiple PropertySourcesPlaceholderConfigurer beans registered"); - } - - @Test - public void overridingPropertiesWithPlaceholderResolutionInEnvShouldOverride() { - 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 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); - } - - @Test - public void unboundElementsFromSystemEnvironmentShouldNotThrowException() { - this.context = new AnnotationConfigApplicationContext(); - ConfigurableEnvironment env = this.context.getEnvironment(); - MutablePropertySources propertySources = env.getPropertySources(); - propertySources.addFirst(new MapPropertySource("test", - Collections.singletonMap("com.example.foo", 5))); - propertySources.addLast(new SystemEnvironmentPropertySource( - StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, - Collections.singletonMap("COM_EXAMPLE_OTHER", "10"))); - this.context.register(TestConfiguration.class); - this.context.refresh(); - int foo = this.context.getBean(TestConfiguration.class).getFoo(); - assertThat(foo).isEqualTo(5); - } - - @Test - public void rebindableConfigurationProperties() { - // gh-9160 - this.context = new AnnotationConfigApplicationContext(); - MutablePropertySources sources = this.context.getEnvironment() - .getPropertySources(); - Map source = new LinkedHashMap<>(); - source.put("example.one", "foo"); - sources.addFirst(new MapPropertySource("test-source", source)); - this.context.register(PrototypePropertiesConfig.class); - this.context.refresh(); - PrototypeBean first = this.context.getBean(PrototypeBean.class); - assertThat(first.getOne()).isEqualTo("foo"); - source.put("example.one", "bar"); - sources.addFirst(new MapPropertySource("extra", - Collections.singletonMap("example.two", "baz"))); - PrototypeBean second = this.context.getBean(PrototypeBean.class); - assertThat(second.getOne()).isEqualTo("bar"); - assertThat(second.getTwo()).isEqualTo("baz"); - } - - @Test - public void rebindableConfigurationPropertiesWithPropertySourcesPlaceholderConfigurer() { - this.context = new AnnotationConfigApplicationContext(); - MutablePropertySources sources = this.context.getEnvironment() - .getPropertySources(); - Map source = new LinkedHashMap<>(); - source.put("example.one", "foo"); - sources.addFirst(new MapPropertySource("test-source", source)); - this.context.register(PrototypePropertiesConfig.class); - this.context.register(PropertySourcesPlaceholderConfigurerConfiguration.class); - this.context.refresh(); - PrototypeBean first = this.context.getBean(PrototypeBean.class); - assertThat(first.getOne()).isEqualTo("foo"); - source.put("example.one", "bar"); - sources.addFirst(new MapPropertySource("extra", - Collections.singletonMap("example.two", "baz"))); - PrototypeBean second = this.context.getBean(PrototypeBean.class); - assertThat(second.getOne()).isEqualTo("bar"); - assertThat(second.getTwo()).isEqualTo("baz"); - } - - @Test - public void converterIsFound() { - prepareConverterContext(ConverterConfiguration.class, PersonProperty.class); - this.context.refresh(); - Person person = this.context.getBean(PersonProperty.class).getPerson(); - assertThat(person.firstName).isEqualTo("John"); - assertThat(person.lastName).isEqualTo("Smith"); - } - - @Test - public void converterWithoutQualifierIsNotInvoked() { - prepareConverterContext(NonQualifiedConverterConfiguration.class, - PersonProperty.class); - this.thrown.expect(BeanCreationException.class); - this.thrown.expectCause(instanceOf(BindException.class)); - this.context.refresh(); - } - - @Test - public void genericConverterIsFound() { - prepareConverterContext(GenericConverterConfiguration.class, - PersonProperty.class); - this.context.refresh(); - Person person = this.context.getBean(PersonProperty.class).getPerson(); - assertThat(person.firstName).isEqualTo("John"); - assertThat(person.lastName).isEqualTo("Smith"); - } - - @Test - public void genericConverterWithoutQualifierIsNotInvoked() { - prepareConverterContext(NonQualifiedGenericConverterConfiguration.class, - PersonProperty.class); - this.thrown.expect(BeanCreationException.class); - this.thrown.expectCause(instanceOf(BindException.class)); - this.context.refresh(); - } - - @Test - public void bindToMapWithNumericKey() { - this.context = new AnnotationConfigApplicationContext(); - MutablePropertySources sources = this.context.getEnvironment() - .getPropertySources(); - Map source = new LinkedHashMap<>(); - source.put("sample.foos.1.name", "One"); - sources.addFirst(new MapPropertySource("test-source", source)); - this.context.register(NumericKeyConfiguration.class); - this.context.refresh(); - NumericKeyConfiguration foo = this.context.getBean(NumericKeyConfiguration.class); - assertThat(foo.getFoos().get("1")).isNotNull(); - } - - @SuppressWarnings("rawtypes") - @Test - public void bindToBeanWithGenerics() { - this.context = new AnnotationConfigApplicationContext(); - MutablePropertySources sources = this.context.getEnvironment() - .getPropertySources(); - Map source = new LinkedHashMap<>(); - source.put("foo.bar", "hello"); - sources.addFirst(new MapPropertySource("test-source", source)); - this.context.register(GenericConfiguration.class); - this.context.refresh(); - AGenericClass foo = this.context.getBean(AGenericClass.class); - assertThat(foo.getBar()).isNotNull(); - } - - private void prepareConverterContext(Class... config) { - this.context = new AnnotationConfigApplicationContext(); - MutablePropertySources sources = this.context.getEnvironment() - .getPropertySources(); - sources.addFirst(new MapPropertySource("test", - Collections.singletonMap("test.person", "John Smith"))); - this.context.register(config); - } - - @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 - 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 - @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 - public static class PrototypePropertiesConfig { - - @Bean - @Scope("prototype") - @ConfigurationProperties("example") - public PrototypeBean prototypeBean() { - return new PrototypeBean(); - } - - } - - @Configuration - public static class PropertySourcesPlaceholderConfigurerConfiguration { - - @Bean - public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } - - } - - public static class PrototypeBean { - - private String one; - - private String two; - - public String getOne() { - return this.one; - } - - public void setOne(String one) { - this.one = one; - } - - public String getTwo() { - return this.two; - } - - public void setTwo(String two) { - this.two = two; - } - - } - - @Configuration - @EnableConfigurationProperties - public static class ConfigurationPropertiesWithFactoryBean { - - public static boolean factoryBeanInit; - - } - - @SuppressWarnings("rawtypes") - // Must be a raw type - static class FactoryBeanTester implements FactoryBean, InitializingBean { - - @Override - public Object getObject() { - return Object.class; - } - - @Override - public Class getObjectType() { - return null; - } - - @Override - public boolean isSingleton() { - return true; - } - - @Override - public void afterPropertiesSet() { - ConfigurationPropertiesWithFactoryBean.factoryBeanInit = true; - } - - } - - @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; - } - - } - - @Configuration - static class ConverterConfiguration { - - @Bean - @ConfigurationPropertiesBinding - public Converter personConverter() { - return new PersonConverter(); - } - - } - - @Configuration - static class NonQualifiedConverterConfiguration { - - @Bean - public Converter personConverter() { - return new PersonConverter(); - } - - } - - private static class PersonConverter implements Converter { - - @Nullable - @Override - public Person convert(String source) { - String[] content = StringUtils.split(source, " "); - return new Person(content[0], content[1]); - } - } - - @Configuration - static class GenericConverterConfiguration { - - @Bean - @ConfigurationPropertiesBinding - public GenericConverter genericPersonConverter() { - return new GenericPersonConverter(); - } - - } - - @Configuration - static class NonQualifiedGenericConverterConfiguration { - - @Bean - public GenericConverter genericPersonConverter() { - return new GenericPersonConverter(); - } - - } - - private static class GenericPersonConverter implements GenericConverter { - - @Nullable - @Override - public Set getConvertibleTypes() { - return Collections.singleton(new ConvertiblePair(String.class, Person.class)); - } - - @Nullable - @Override - public Object convert(@Nullable Object source, TypeDescriptor sourceType, - TypeDescriptor targetType) { - String[] content = StringUtils.split((String) source, " "); - return new Person(content[0], content[1]); - } - } - - @Configuration - @EnableConfigurationProperties - @ConfigurationProperties(prefix = "test") - public static class PersonProperty { - - private Person person; - - public Person getPerson() { - return this.person; - } - - public void setPerson(Person person) { - this.person = person; - } - - } - - static class Person { - - private final String firstName; - - private final String lastName; - - Person(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - - } - - @Configuration - @EnableConfigurationProperties - @ConfigurationProperties(prefix = "sample") - static class NumericKeyConfiguration { - - private Map foos = new LinkedHashMap<>(); - - public Map getFoos() { - return this.foos; - } - - static class Foo { - - private String name; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - } - - } - - @Configuration - @EnableConfigurationProperties - static class GenericConfiguration { - - @Bean - @ConfigurationProperties("foo") - public AGenericClass aBeanToBind() { - return new AGenericClass<>(); - } - - } - - static class AGenericClass { - - private T bar; - - public T getBar() { - return this.bar; - } - - public void setBar(T bar) { - this.bar = bar; - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java new file mode 100644 index 0000000000..cd32be94ff --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -0,0 +1,1572 @@ +/* + * Copyright 2012-2018 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.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import org.hamcrest.Matchers; +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.Autowired; +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.testsupport.rule.OutputCapture; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.context.annotation.Scope; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; +import org.springframework.lang.Nullable; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.stereotype.Component; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.fail; + +/** + * Tests for {@link ConfigurationProperties} annotated beans. Covers + * {@link EnableConfigurationProperties}, + * {@link ConfigurationPropertiesBindingPostProcessorRegistrar}, + * {@link ConfigurationPropertiesBindingPostProcessor} and + * {@link ConfigurationPropertiesBinder}. + * + * @author Dave Syer + * @author Christian Dupuis + * @author Phillip Webb + * @author Stephane Nicoll + * @author Madhura Bhave + */ +public class ConfigurationPropertiesTests { + + private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public OutputCapture output = new OutputCapture(); + + @After + public void cleanup() { + this.context.close(); + System.clearProperty("name"); + System.clearProperty("nested.name"); + System.clearProperty("nested_name"); + } + + @Test + public void loadShouldBind() { + load(BasicConfiguration.class, "name=foo"); + assertThat(this.context.getBeanNamesForType(BasicProperties.class)).hasSize(1); + assertThat(this.context.containsBean(BasicProperties.class.getName())).isTrue(); + assertThat(this.context.getBean(BasicProperties.class).name).isEqualTo("foo"); + } + + @Test + public void loadShouldBindNested() { + load(NestedConfiguration.class, "name=foo", "nested.name=bar"); + assertThat(this.context.getBeanNamesForType(NestedProperties.class)).hasSize(1); + assertThat(this.context.getBean(NestedProperties.class).name).isEqualTo("foo"); + assertThat(this.context.getBean(NestedProperties.class).nested.name) + .isEqualTo("bar"); + } + + @Test + public void loadWhenUsingSystemPropertiesShouldBind() { + System.setProperty("name", "foo"); + load(BasicConfiguration.class); + assertThat(this.context.getBeanNamesForType(BasicProperties.class)).hasSize(1); + assertThat(this.context.getBean(BasicProperties.class).name).isEqualTo("foo"); + } + + @Test + public void loadWhenUsingSystemPropertiesShouldBindNested() { + System.setProperty("name", "foo"); + System.setProperty("nested.name", "bar"); + load(NestedConfiguration.class); + assertThat(this.context.getBeanNamesForType(NestedProperties.class)).hasSize(1); + assertThat(this.context.getBean(NestedProperties.class).name).isEqualTo("foo"); + assertThat(this.context.getBean(NestedProperties.class).nested.name) + .isEqualTo("bar"); + } + + @Test + public void loadWhenHasIgnoreUnknownFieldsFalseAndNoUnknownFieldsShouldBind() { + removeSystemProperties(); + load(IgnoreUnknownFieldsFalseConfiguration.class, "name=foo"); + IgnoreUnknownFieldsFalseProperties bean = this.context + .getBean(IgnoreUnknownFieldsFalseProperties.class); + assertThat(((BasicProperties) bean).name).isEqualTo("foo"); + } + + @Test + public void loadWhenHasIgnoreUnknownFieldsFalseAndUnknownFieldsShouldFail() { + removeSystemProperties(); + this.thrown.expectCause(Matchers.instanceOf(BindException.class)); + load(IgnoreUnknownFieldsFalseConfiguration.class, "name=foo", "bar=baz"); + } + + @Test + public void loadWhenHasIgnoreInvalidFieldsTrueAndInvalidFieldsShouldBind() { + load(IgnoreInvalidFieldsFalseProperties.class, "com.example.bar=spam"); + IgnoreInvalidFieldsFalseProperties bean = this.context + .getBean(IgnoreInvalidFieldsFalseProperties.class); + assertThat(bean.getBar()).isEqualTo(0); + } + + @Test + public void loadWhenHasPrefixShouldBind() { + load(PrefixConfiguration.class, "spring.foo.name=foo"); + PrefixProperties bean = this.context.getBean(PrefixProperties.class); + assertThat(((BasicProperties) bean).name).isEqualTo("foo"); + } + + @Test + public void loadWhenPropertiesHaveAnnotationOnBaseClassShouldBind() { + load(AnnotationOnBaseClassConfiguration.class, "name=foo"); + AnnotationOnBaseClassProperties bean = this.context + .getBean(AnnotationOnBaseClassProperties.class); + assertThat(((BasicProperties) bean).name).isEqualTo("foo"); + } + + @Test + public void loadWhenBindingArrayShouldBind() { + load(BasicConfiguration.class, "name=foo", "array=1,2,3"); + BasicProperties bean = this.context.getBean(BasicProperties.class); + assertThat(bean.array).containsExactly(1, 2, 3); + } + + @Test + public void loadWhenBindingArrayFromYamlArrayShouldBind() { + load(BasicConfiguration.class, "name=foo", "list[0]=1", "list[1]=2", "list[2]=3"); + BasicProperties bean = this.context.getBean(BasicProperties.class); + assertThat(bean.list).containsExactly(1, 2, 3); + } + + @Test + public void loadWhenBindingOver256ElementsShouldBind() { + List pairs = new ArrayList<>(); + pairs.add("name:foo"); + for (int i = 0; i < 1000; i++) { + pairs.add("list[" + i + "]:" + i); + } + load(BasicConfiguration.class, pairs.toArray(new String[] {})); + BasicProperties bean = this.context.getBean(BasicProperties.class); + assertThat(bean.list).hasSize(1000); + } + + @Test + public void loadWhenBindingWithoutAndAnnotationShouldFail() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("No ConfigurationProperties annotation found"); + load(WithoutAndAnnotationConfiguration.class, "name:foo"); + } + + @Test + public void loadWhenBindingWithoutAnnotationValueShouldBind() { + load(WithoutAnnotationValueConfiguration.class, "name=foo"); + WithoutAnnotationValueProperties bean = this.context + .getBean(WithoutAnnotationValueProperties.class); + assertThat(bean.name).isEqualTo("foo"); + } + + @Test + public void loadWhenBindingWithDefaultsInXmlShouldBind() { + load(new Class[] { BasicConfiguration.class, + DefaultsInXmlConfiguration.class }); + BasicProperties bean = this.context.getBean(BasicProperties.class); + assertThat(bean.name).isEqualTo("bar"); + } + + @Test + public void loadWhenBindingWithDefaultsInJavaConfigurationShouldBind() { + load(DefaultsInJavaConfiguration.class); + BasicProperties bean = this.context.getBean(BasicProperties.class); + assertThat(bean.name).isEqualTo("bar"); + } + + @Test + public void loadWhenBindingTwoBeansShouldBind() { + load(new Class[] { WithoutAnnotationValueConfiguration.class, + BasicConfiguration.class }); + assertThat(this.context.getBean(BasicProperties.class)).isNotNull(); + assertThat(this.context.getBean(WithoutAnnotationValueProperties.class)) + .isNotNull(); + } + + @Test + public void loadWhenBindingWithParentContextShouldBind() { + AnnotationConfigApplicationContext parent = load(BasicConfiguration.class, + "name=parent"); + this.context = new AnnotationConfigApplicationContext(); + this.context.setParent(parent); + load(new Class[] { BasicConfiguration.class, BasicPropertiesConsumer.class }, + "name=child"); + assertThat(this.context.getBean(BasicProperties.class)).isNotNull(); + assertThat(parent.getBean(BasicProperties.class)).isNotNull(); + assertThat(this.context.getBean(BasicPropertiesConsumer.class).getName()) + .isEqualTo("parent"); + parent.close(); + } + + @Test + public void loadWhenBindingOnlyParentContextShouldBind() { + AnnotationConfigApplicationContext parent = load(BasicConfiguration.class, + "name=foo"); + this.context = new AnnotationConfigApplicationContext(); + this.context.setParent(parent); + load(BasicPropertiesConsumer.class); + assertThat(this.context.getBeanNamesForType(BasicProperties.class)).isEmpty(); + assertThat(parent.getBeanNamesForType(BasicProperties.class)).hasSize(1); + assertThat(this.context.getBean(BasicPropertiesConsumer.class).getName()) + .isEqualTo("foo"); + } + + @Test + public void loadWhenPrefixedPropertiesDecalredAsBeanShouldBind() { + load(PrefixPropertiesDecalredAsBeanConfiguration.class, "spring.foo.name=foo"); + PrefixProperties bean = this.context.getBean(PrefixProperties.class); + assertThat(((BasicProperties) bean).name).isEqualTo("foo"); + } + + @Test + public void loadWhenPrefixedPropertiesDecalredAsAnnotationValueShouldBind() { + load(PrefixPropertiesDecalredAsAnnotationValueConfiguration.class, + "spring.foo.name=foo"); + PrefixProperties bean = this.context.getBean( + "spring.foo-" + PrefixProperties.class.getName(), PrefixProperties.class); + assertThat(((BasicProperties) bean).name).isEqualTo("foo"); + } + + @Test + public void loadWhenMultiplePrefixedPropertiesDecalredAsAnnotationValueShouldBind() { + load(MultiplePrefixPropertiesDecalredAsAnnotationValueConfiguration.class, + "spring.foo.name=foo", "spring.bar.name=bar"); + PrefixProperties bean1 = this.context.getBean(PrefixProperties.class); + AnotherPrefixProperties bean2 = this.context + .getBean(AnotherPrefixProperties.class); + assertThat(((BasicProperties) bean1).name).isEqualTo("foo"); + assertThat(((BasicProperties) bean2).name).isEqualTo("bar"); + } + + @Test + public void loadWhenBindingToMapKeyWithPeriodShouldBind() { + load(MapProperties.class, "mymap.key1.key2:value12", "mymap.key3:value3"); + MapProperties bean = this.context.getBean(MapProperties.class); + assertThat(bean.mymap).containsOnly(entry("key3", "value3"), + entry("key1.key2", "value12")); + } + + @Test + public void loadWhenPrefixedPropertiesAreReplacedOnBeanMethodShouldBind() { + load(PrefixedPropertiesReplacedOnBeanMethodConfiguration.class, + "external.name=bar", "spam.name=foo"); + PrefixProperties bean = this.context.getBean(PrefixProperties.class); + assertThat(((BasicProperties) bean).name).isEqualTo("foo"); + } + + @Test + public void loadShouldBindToJavaTimeDuration() { + load(BasicConfiguration.class, "duration=PT1M"); + BasicProperties bean = this.context.getBean(BasicProperties.class); + assertThat(bean.getDuration().getSeconds()).isEqualTo(60); + } + + @Test + public void loadWhenBindingToValidatedImplementationOfInterfaceShouldBind() { + load(ValidatedImplementationConfiguration.class, "test.foo=bar"); + ValidatedImplementationProperties bean = this.context + .getBean(ValidatedImplementationProperties.class); + assertThat(bean.getFoo()).isEqualTo("bar"); + } + + @Test + public void bindWithValueDefault() { + load(WithPropertyPlaceholderValueConfiguration.class, "default.value=foo"); + WithPropertyPlaceholderValueProperties bean = this.context + .getBean(WithPropertyPlaceholderValueProperties.class); + assertThat(bean.getValue()).isEqualTo("foo"); + } + + @Test + public void loadWhenHasPostConstructShouldTriggerPostConstructWithBoundBean() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("bar", "foo"); + this.context.setEnvironment(environment); + this.context.register(WithPostConstructConfiguration.class); + this.context.refresh(); + WithPostConstructConfiguration bean = this.context + .getBean(WithPostConstructConfiguration.class); + assertThat(bean.initialized).isTrue(); + } + + @Test + public void loadShouldNotInitializeFactoryBeans() { + WithFactoryBeanConfiguration.factoryBeanInitialized = false; + this.context = new AnnotationConfigApplicationContext() { + + @Override + protected void onRefresh() throws BeansException { + assertThat(WithFactoryBeanConfiguration.factoryBeanInitialized) + .as("Initialized too early").isFalse(); + super.onRefresh(); + } + + }; + this.context.register(WithFactoryBeanConfiguration.class); + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(FactoryBeanTester.class); + beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + this.context.registerBeanDefinition("test", beanDefinition); + this.context.refresh(); + assertThat(WithFactoryBeanConfiguration.factoryBeanInitialized) + .as("Not Initialized").isTrue(); + } + + @Test + public void loadWhenUsingRelaxedFormsShouldBindToEnum() { + bindToEnum("test.theValue=FOO"); + bindToEnum("test.theValue=foo"); + bindToEnum("test.the-value=FoO"); + bindToEnum("test.THE_VALUE=FoO"); + } + + private void bindToEnum(String... inlinedProperties) { + load(WithEnumProperties.class, inlinedProperties); + WithEnumProperties bean = this.context.getBean(WithEnumProperties.class); + assertThat(bean.getTheValue()).isEqualTo(FooEnum.FOO); + resetContext(); + } + + @Test + public void loadWhenUsingRelaxedFormsShouldBindToEnumSet() { + bindToEnumSet("test.the-values=foo,bar", FooEnum.FOO, FooEnum.BAR); + bindToEnumSet("test.the-values=foo", FooEnum.FOO); + } + + private void bindToEnumSet(String inlinedProperty, FooEnum... expected) { + load(WithEnumProperties.class, inlinedProperty); + WithEnumProperties bean = this.context.getBean(WithEnumProperties.class); + assertThat(bean.getTheValues()).contains(expected); + resetContext(); + } + + @Test + public void loadShouldBindToCharArray() { + load(WithCharArrayProperties.class, "test.chars=word"); + WithCharArrayProperties bean = this.context + .getBean(WithCharArrayProperties.class); + assertThat(bean.getChars()).isEqualTo("word".toCharArray()); + } + + @Test + public void loadWhenUsingRelaxedFormsAndOverrideShouldBind() { + load(WithRelaxedNamesProperties.class, "test.FOO_BAR=test1", "test.FOO_BAR=test2", + "test.BAR-B-A-Z=testa", "test.BAR-B-A-Z=testb"); + WithRelaxedNamesProperties bean = this.context + .getBean(WithRelaxedNamesProperties.class); + assertThat(bean.getFooBar()).isEqualTo("test2"); + assertThat(bean.getBarBAZ()).isEqualTo("testb"); + } + + @Test + public void loadShouldBindToMap() { + load(WithMapProperties.class, "test.map.foo=bar"); + WithMapProperties bean = this.context.getBean(WithMapProperties.class); + assertThat(bean.getMap()).containsOnly(entry("foo", "bar")); + } + + @Test + public void loadShouldBindToMapWithNumericKey() { + load(MapWithNumericKeyProperties.class, "sample.properties.1.name=One"); + MapWithNumericKeyProperties bean = this.context + .getBean(MapWithNumericKeyProperties.class); + assertThat(bean.getProperties().get("1").name).isEqualTo("One"); + } + + @Test + public void loadWhenUsingSystemPropertiesShouldBindToMap() { + this.context.getEnvironment().getPropertySources() + .addLast(new SystemEnvironmentPropertySource( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + Collections.singletonMap("TEST_MAP_FOO_BAR", "baz"))); + load(WithComplexMapProperties.class); + WithComplexMapProperties bean = this.context + .getBean(WithComplexMapProperties.class); + assertThat(bean.getMap()).containsOnlyKeys("foo"); + assertThat(bean.getMap().get("foo")).containsOnly(entry("bar", "baz")); + } + + @Test + public void loadWhenOverridingPropertiesShouldBind() { + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + sources.addFirst(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("SPRING_FOO_NAME", "Jane"))); + sources.addLast(new MapPropertySource("test", + Collections.singletonMap("spring.foo.name", "John"))); + load(PrefixConfiguration.class); + BasicProperties bean = this.context.getBean(BasicProperties.class); + assertThat(bean.name).isEqualTo("Jane"); + } + + @Test + public void loadWhenJsr303ConstraintDoesNotMatchShouldFail() { + this.thrown.expectCause(Matchers.instanceOf(BindException.class)); + load(ValidatedJsr303Configuration.class, "description="); + } + + @Test + public void loadValidatedOnBeanMethodAndJsr303ConstraintDoesNotMatchShouldFail() { + this.thrown.expectCause(Matchers.instanceOf(BindException.class)); + load(ValidatedOnBeanJsr303Configuration.class, "description="); + } + + @Test + public void loadWhenJsr303ConstraintDoesNotMatchOnNestedThatIsNotDirectlyAnnotatedShouldFail() { + this.thrown.expectCause(Matchers.instanceOf(BindException.class)); + load(ValidatedNestedJsr303Properties.class, "properties.description="); + } + + @Test + public void loadWhenJsr303ConstraintDoesNotMatchOnNestedThatIsNotDirectlyAnnotatedButIsValidShouldFail() { + this.thrown.expectCause(Matchers.instanceOf(BindException.class)); + load(ValidatedValidNestedJsr303Properties.class); + } + + @Test + public void loadWhenJsr303ConstraintMatchesShouldBind() { + load(ValidatedJsr303Configuration.class, "description=foo"); + ValidatedJsr303Properties bean = this.context + .getBean(ValidatedJsr303Properties.class); + assertThat(bean.getDescription()).isEqualTo("foo"); + } + + @Test + public void loadWhenJsr303ConstraintDoesNotMatchAndNotValidatedAnnotationShouldBind() { + load(NonValidatedJsr303Configuration.class, "name=foo"); + NonValidatedJsr303Properties bean = this.context + .getBean(NonValidatedJsr303Properties.class); + assertThat(((BasicProperties) bean).name).isEqualTo("foo"); + } + + @Test + public void loadWhenHasMultiplePropertySourcesPlaceholderConfigurerShouldLogWarning() { + load(MultiplePropertySourcesPlaceholderConfigurerConfiguration.class); + assertThat(this.output.toString()).contains( + "Multiple PropertySourcesPlaceholderConfigurer beans registered"); + } + + @Test + public void loadWhenOverridingPropertiesWithPlaceholderResolutionInEnvionmentShouldBindWithOverride() { + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + sources.addFirst(new SystemEnvironmentPropertySource("system", + Collections.singletonMap("COM_EXAMPLE_BAR", "10"))); + Map source = new HashMap<>(); + source.put("com.example.bar", 5); + source.put("com.example.foo", "${com.example.bar}"); + sources.addLast(new MapPropertySource("test", source)); + load(SimplePrefixedProperties.class); + SimplePrefixedProperties bean = this.context + .getBean(SimplePrefixedProperties.class); + assertThat(bean.getFoo()).isEqualTo(10); + } + + @Test + public void loadWhenHasUnboundElementsFromSystemEnvironmentShouldNotThrowException() { + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + sources.addFirst(new MapPropertySource("test", + Collections.singletonMap("com.example.foo", 5))); + sources.addLast(new SystemEnvironmentPropertySource( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + Collections.singletonMap("COM_EXAMPLE_OTHER", "10"))); + load(SimplePrefixedProperties.class); + SimplePrefixedProperties bean = this.context + .getBean(SimplePrefixedProperties.class); + assertThat(bean.getFoo()).isEqualTo(5); + } + + @Test + public void loadShouldSupportRebindableConfigurationProperties() { + // gh-9160 + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + Map source = new LinkedHashMap<>(); + source.put("example.one", "foo"); + sources.addFirst(new MapPropertySource("test-source", source)); + this.context.register(PrototypePropertiesConfiguration.class); + this.context.refresh(); + PrototypeBean first = this.context.getBean(PrototypeBean.class); + assertThat(first.getOne()).isEqualTo("foo"); + source.put("example.one", "bar"); + sources.addFirst(new MapPropertySource("extra", + Collections.singletonMap("example.two", "baz"))); + PrototypeBean second = this.context.getBean(PrototypeBean.class); + assertThat(second.getOne()).isEqualTo("bar"); + assertThat(second.getTwo()).isEqualTo("baz"); + } + + @Test + public void loadWhenHasPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties() { + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + Map source = new LinkedHashMap<>(); + source.put("example.one", "foo"); + sources.addFirst(new MapPropertySource("test-source", source)); + this.context.register(PrototypePropertiesConfiguration.class); + this.context.register(PropertySourcesPlaceholderConfigurer.class); + this.context.refresh(); + PrototypeBean first = this.context.getBean(PrototypeBean.class); + assertThat(first.getOne()).isEqualTo("foo"); + source.put("example.one", "bar"); + sources.addFirst(new MapPropertySource("extra", + Collections.singletonMap("example.two", "baz"))); + PrototypeBean second = this.context.getBean(PrototypeBean.class); + assertThat(second.getOne()).isEqualTo("bar"); + assertThat(second.getTwo()).isEqualTo("baz"); + } + + @Test + public void loadShouldUseConfigurationConverter() { + prepareConverterContext(ConverterConfiguration.class, PersonProperties.class); + Person person = this.context.getBean(PersonProperties.class).getPerson(); + assertThat(person.firstName).isEqualTo("John"); + assertThat(person.lastName).isEqualTo("Smith"); + } + + @Test + public void loadWhenConfigurationConverterIsNotQualifiedShouldNotConvert() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectCause(instanceOf(BindException.class)); + prepareConverterContext(NonQualifiedConverterConfiguration.class, + PersonProperties.class); + } + + @Test + public void loadShouldUseGenericConfigurationConverter() { + prepareConverterContext(GenericConverterConfiguration.class, + PersonProperties.class); + Person person = this.context.getBean(PersonProperties.class).getPerson(); + assertThat(person.firstName).isEqualTo("John"); + assertThat(person.lastName).isEqualTo("Smith"); + } + + @Test + public void loadWhenGenericConfigurationConverterIsNotQualifiedShouldNotConvert() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectCause(instanceOf(BindException.class)); + prepareConverterContext(NonQualifiedGenericConverterConfiguration.class, + PersonProperties.class); + } + + @Test + @SuppressWarnings("rawtypes") + public void loadShouldBindToBeanWithGenerics() { + load(GenericConfiguration.class, "foo.bar=hello"); + AGenericClass foo = this.context.getBean(AGenericClass.class); + assertThat(foo.getBar()).isNotNull(); + } + + private void prepareConverterContext(Class... config) { + load(config, "test.person=John Smith"); + } + + @Test + public void loadWhenHasConfigurationPropertiesValidatorShouldApplyValidator() { + try { + load(WithCustomValidatorConfiguration.class); + fail("Did not throw"); + } + catch (Exception ex) { + assertThat(ex).hasCauseInstanceOf(BindException.class); + assertThat(ex.getCause()) + .hasCauseExactlyInstanceOf(BindValidationException.class); + } + } + + @Test + public void loadWhenHasUnsupportedConfigurationPropertiesValidatorShouldBind() { + load(WithUnsupportedCustomValidatorConfiguration.class, "test.foo=bar"); + WithSetterThatThrowsValidationExceptionProperties bean = this.context + .getBean(WithSetterThatThrowsValidationExceptionProperties.class); + assertThat(bean.getFoo()).isEqualTo("bar"); + } + + @Test + public void loadWhenConfigurationPropertiesIsAlsoValidatorShouldApplyValidator() { + try { + load(ValidatorProperties.class); + fail("Did not throw"); + } + catch (Exception ex) { + assertThat(ex).hasCauseInstanceOf(BindException.class); + assertThat(ex.getCause()) + .hasCauseExactlyInstanceOf(BindValidationException.class); + } + } + + @Test + public void loadWhenSetterThrowsValidationExceptionShouldFail() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectCause(instanceOf(BindException.class)); + load(WithSetterThatThrowsValidationExceptionProperties.class, "test.foo=spam"); + } + + @Test + public void loadWhenFailsShouldIncludeAnnotationDetails() { + removeSystemProperties(); + this.thrown.expectMessage("Could not bind properties to " + + "'ConfigurationPropertiesTests.IgnoreUnknownFieldsFalseProperties' : " + + "prefix=, ignoreInvalidFields=false, ignoreUnknownFields=false;"); + load(IgnoreUnknownFieldsFalseConfiguration.class, "name=foo", "bar=baz"); + } + + private AnnotationConfigApplicationContext load(Class configuration, + String... inlinedProperties) { + return load(new Class[] { configuration }, inlinedProperties); + } + + private AnnotationConfigApplicationContext load(Class[] configuration, + String... inlinedProperties) { + this.context.register(configuration); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, + inlinedProperties); + this.context.refresh(); + return this.context; + } + + /** + * Strict tests need a known set of properties so we remove system items which may be + * environment specific. + */ + private void removeSystemProperties() { + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + sources.remove("systemProperties"); + sources.remove("systemEnvironment"); + } + + private void resetContext() { + this.context.close(); + this.context = new AnnotationConfigApplicationContext(); + } + + @Configuration + @EnableConfigurationProperties(BasicProperties.class) + static class BasicConfiguration { + + } + + @Configuration + @EnableConfigurationProperties(NestedProperties.class) + static class NestedConfiguration { + + } + + @Configuration + @EnableConfigurationProperties(IgnoreUnknownFieldsFalseProperties.class) + static class IgnoreUnknownFieldsFalseConfiguration { + + } + + @Configuration + @EnableConfigurationProperties(PrefixProperties.class) + static class PrefixConfiguration { + + } + + @Configuration + @EnableConfigurationProperties(ValidatedJsr303Properties.class) + static class ValidatedJsr303Configuration { + + } + + @Configuration + @EnableConfigurationProperties + static class ValidatedOnBeanJsr303Configuration { + + @Bean + @Validated + public NonValidatedJsr303Properties properties() { + return new NonValidatedJsr303Properties(); + } + } + + @Configuration + @EnableConfigurationProperties(NonValidatedJsr303Properties.class) + static class NonValidatedJsr303Configuration { + + } + + @Configuration + @EnableConfigurationProperties(AnnotationOnBaseClassProperties.class) + static class AnnotationOnBaseClassConfiguration { + + } + + @Configuration + @EnableConfigurationProperties(WithoutAndAnnotationConfiguration.class) + static class WithoutAndAnnotationConfiguration { + + } + + @Configuration + @EnableConfigurationProperties(WithoutAnnotationValueProperties.class) + static class WithoutAnnotationValueConfiguration { + + } + + @Configuration + @ImportResource("org/springframework/boot/context/properties/testProperties.xml") + static class DefaultsInXmlConfiguration { + + } + + @Configuration + static class DefaultsInJavaConfiguration { + + @Bean + public BasicProperties basicProperties() { + BasicProperties test = new BasicProperties(); + test.setName("bar"); + return test; + } + + } + + @Configuration + @EnableConfigurationProperties + static class PrefixPropertiesDecalredAsBeanConfiguration { + + @Bean + public PrefixProperties prefixProperties() { + return new PrefixProperties(); + } + + } + + @Configuration + @EnableConfigurationProperties(PrefixProperties.class) + static class PrefixPropertiesDecalredAsAnnotationValueConfiguration { + + } + + @Configuration + @EnableConfigurationProperties({ PrefixProperties.class, + AnotherPrefixProperties.class }) + static class MultiplePrefixPropertiesDecalredAsAnnotationValueConfiguration { + + } + + @Configuration + @EnableConfigurationProperties + static class PrefixedPropertiesReplacedOnBeanMethodConfiguration { + + @Bean + @ConfigurationProperties(prefix = "spam") + public PrefixProperties prefixProperties() { + return new PrefixProperties(); + } + + } + + @Configuration + @EnableConfigurationProperties + static class ValidatedImplementationConfiguration { + + @Bean + public ValidatedImplementationProperties testProperties() { + return new ValidatedImplementationProperties(); + } + + } + + @Configuration + @EnableConfigurationProperties + @ConfigurationProperties + static class WithPostConstructConfiguration { + + private String bar; + + private boolean initialized; + + public void setBar(String bar) { + this.bar = bar; + } + + public String getBar() { + return this.bar; + } + + @PostConstruct + public void init() { + assertThat(this.bar).isNotNull(); + this.initialized = true; + } + + } + + @Configuration + @EnableConfigurationProperties(WithPropertyPlaceholderValueProperties.class) + static class WithPropertyPlaceholderValueConfiguration { + + @Bean + public static PropertySourcesPlaceholderConfigurer configurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + + } + + @Configuration + @EnableConfigurationProperties + static class WithFactoryBeanConfiguration { + + public static boolean factoryBeanInitialized; + + } + + @Configuration + @EnableConfigurationProperties + static class MultiplePropertySourcesPlaceholderConfigurerConfiguration { + + @Bean + public static PropertySourcesPlaceholderConfigurer configurer1() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + public static PropertySourcesPlaceholderConfigurer configurer2() { + return new PropertySourcesPlaceholderConfigurer(); + } + + } + + @Configuration + @EnableConfigurationProperties + static class PrototypePropertiesConfiguration { + + @Bean + @Scope("prototype") + @ConfigurationProperties("example") + public PrototypeBean prototypeBean() { + return new PrototypeBean(); + } + + } + + @Configuration + static class ConverterConfiguration { + + @Bean + @ConfigurationPropertiesBinding + public Converter personConverter() { + return new PersonConverter(); + } + + } + + @Configuration + static class NonQualifiedConverterConfiguration { + + @Bean + public Converter personConverter() { + return new PersonConverter(); + } + + } + + @Configuration + static class GenericConverterConfiguration { + + @Bean + @ConfigurationPropertiesBinding + public GenericConverter genericPersonConverter() { + return new GenericPersonConverter(); + } + + } + + @Configuration + static class NonQualifiedGenericConverterConfiguration { + + @Bean + public GenericConverter genericPersonConverter() { + return new GenericPersonConverter(); + } + + } + + @Configuration + @EnableConfigurationProperties + static class GenericConfiguration { + + @Bean + @ConfigurationProperties("foo") + public AGenericClass aBeanToBind() { + return new AGenericClass<>(); + } + + } + + @Configuration + @EnableConfigurationProperties(WithCustomValidatorProperties.class) + static class WithCustomValidatorConfiguration { + + @Bean(name = ConfigurationPropertiesBindingPostProcessor.VALIDATOR_BEAN_NAME) + public CustomPropertiesValidator validator() { + return new CustomPropertiesValidator(); + } + + } + + @Configuration + @EnableConfigurationProperties(WithSetterThatThrowsValidationExceptionProperties.class) + static class WithUnsupportedCustomValidatorConfiguration { + + @Bean(name = ConfigurationPropertiesBindingPostProcessor.VALIDATOR_BEAN_NAME) + public CustomPropertiesValidator validator() { + return new CustomPropertiesValidator(); + } + + } + + static class AGenericClass { + + private T bar; + + public T getBar() { + return this.bar; + } + + public void setBar(T bar) { + this.bar = bar; + } + + } + + static class PrototypeBean { + + private String one; + + private String two; + + public String getOne() { + return this.one; + } + + public void setOne(String one) { + this.one = one; + } + + public String getTwo() { + return this.two; + } + + public void setTwo(String two) { + this.two = two; + } + + } + + // Must be a raw type + @SuppressWarnings("rawtypes") + static class FactoryBeanTester implements FactoryBean, InitializingBean { + + @Override + public Object getObject() { + return Object.class; + } + + @Override + public Class getObjectType() { + return null; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public void afterPropertiesSet() { + WithFactoryBeanConfiguration.factoryBeanInitialized = true; + } + + } + + @ConfigurationProperties + static class BasicProperties { + + private String name; + + private int[] array; + + private List list = new ArrayList<>(); + + private Duration duration; + + // No getter - you should be able to bind to a write-only bean + + public void setName(String name) { + this.name = name; + } + + public void setArray(int... values) { + this.array = values; + } + + public int[] getArray() { + return this.array; + } + + public List getList() { + return this.list; + } + + public void setList(List list) { + this.list = list; + } + + public Duration getDuration() { + return this.duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + } + + @ConfigurationProperties + static class NestedProperties { + + private String name; + + private final Nested nested = new Nested(); + + public void setName(String name) { + this.name = name; + } + + public Nested getNested() { + return this.nested; + } + + protected static class Nested { + + private String name; + + public void setName(String name) { + this.name = name; + } + + } + + } + + @ConfigurationProperties(ignoreUnknownFields = false) + static class IgnoreUnknownFieldsFalseProperties extends BasicProperties { + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "com.example", ignoreInvalidFields = true) + static class IgnoreInvalidFieldsFalseProperties { + + private long bar; + + public void setBar(long bar) { + this.bar = bar; + } + + public long getBar() { + return this.bar; + } + + } + + @ConfigurationProperties(prefix = "spring.foo") + static class PrefixProperties extends BasicProperties { + + } + + @ConfigurationProperties(prefix = "spring.bar") + static class AnotherPrefixProperties extends BasicProperties { + + } + + static class Jsr303Properties extends BasicProperties { + + @NotEmpty + private String description; + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + } + + @ConfigurationProperties + @Validated + static class ValidatedJsr303Properties extends Jsr303Properties { + + } + + @ConfigurationProperties + static class NonValidatedJsr303Properties extends Jsr303Properties { + + } + + @EnableConfigurationProperties + @ConfigurationProperties + @Validated + static class ValidatedNestedJsr303Properties { + + private Jsr303Properties properties; + + public Jsr303Properties getProperties() { + return this.properties; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties + @Validated + static class ValidatedValidNestedJsr303Properties { + + @Valid + private List properties = Collections + .singletonList(new Jsr303Properties()); + + public List getProperties() { + return this.properties; + } + + } + + static class AnnotationOnBaseClassProperties extends BasicProperties { + + } + + @ConfigurationProperties + static class WithoutAnnotationValueProperties { + + private String name; + + public void setName(String name) { + this.name = name; + } + + // No getter - you should be able to bind to a write-only bean + + } + + @EnableConfigurationProperties + @ConfigurationProperties + static class MapProperties { + + private Map mymap; + + public void setMymap(Map mymap) { + this.mymap = mymap; + } + + public Map getMymap() { + return this.mymap; + } + + } + + @Component + static class BasicPropertiesConsumer { + + @Autowired + private BasicProperties properties; + + @PostConstruct + public void init() { + assertThat(this.properties).isNotNull(); + } + + public String getName() { + return this.properties.name; + } + + } + + interface InterfaceForValidatedImplementation { + + String getFoo(); + } + + @ConfigurationProperties("test") + @Validated + static class ValidatedImplementationProperties + implements InterfaceForValidatedImplementation { + + @NotNull + private String foo; + + @Override + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + + @ConfigurationProperties(prefix = "test") + @Validated + static class WithPropertyPlaceholderValueProperties { + + @Value("${default.value}") + private String value; + + public void setValue(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test") + static class WithEnumProperties { + + private FooEnum theValue; + + private List theValues; + + public void setTheValue(FooEnum value) { + this.theValue = value; + } + + public FooEnum getTheValue() { + return this.theValue; + } + + public List getTheValues() { + return this.theValues; + } + + public void setTheValues(List theValues) { + this.theValues = theValues; + } + + } + + enum FooEnum { + + FOO, BAZ, BAR + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test", ignoreUnknownFields = false) + static class WithCharArrayProperties { + + private char[] chars; + + public char[] getChars() { + return this.chars; + } + + public void setChars(char[] chars) { + this.chars = chars; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test") + static class WithRelaxedNamesProperties { + + 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; + } + + } + + @Validated + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test") + static class WithMapProperties { + + private Map map; + + public Map getMap() { + return this.map; + } + + public void setMap(Map map) { + this.map = map; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test") + static class WithComplexMapProperties { + + private Map> map; + + public Map> getMap() { + return this.map; + } + + public void setMap(Map> map) { + this.map = map; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "com.example", ignoreUnknownFields = false) + static class SimplePrefixedProperties { + + 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; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test") + static class PersonProperties { + + private Person person; + + public Person getPerson() { + return this.person; + } + + public void setPerson(Person person) { + this.person = person; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "sample") + static class MapWithNumericKeyProperties { + + private Map properties = new LinkedHashMap<>(); + + public Map getProperties() { + return this.properties; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties + static class ValidatorProperties implements Validator { + + private String foo; + + @Override + public boolean supports(Class type) { + return type == ValidatorProperties.class; + } + + @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; + } + + } + + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "test") + static class WithSetterThatThrowsValidationExceptionProperties { + + 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"); + } + } + + } + + @ConfigurationProperties(prefix = "custom") + static class WithCustomValidatorProperties { + + private String foo; + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + + static class CustomPropertiesValidator implements Validator { + + @Override + public boolean supports(Class type) { + return type == WithCustomValidatorProperties.class; + } + + @Override + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "foo", "TEST1"); + } + + } + + static class PersonConverter implements Converter { + + @Nullable + @Override + public Person convert(String source) { + String[] content = StringUtils.split(source, " "); + return new Person(content[0], content[1]); + } + } + + static class GenericPersonConverter implements GenericConverter { + + @Nullable + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Person.class)); + } + + @Nullable + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + String[] content = StringUtils.split((String) source, " "); + return new Person(content[0], content[1]); + } + } + + static class Person { + + private final String firstName; + + private final String lastName; + + Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java deleted file mode 100644 index 00ab941562..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java +++ /dev/null @@ -1,679 +0,0 @@ -/* - * 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.ArrayList; -import java.util.List; -import java.util.Map; - -import javax.annotation.PostConstruct; -import javax.validation.constraints.NotNull; - -import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.bind.BindException; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportResource; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.stereotype.Component; -import org.springframework.test.context.support.TestPropertySourceUtils; -import org.springframework.validation.annotation.Validated; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link EnableConfigurationProperties}. - * - * @author Dave Syer - * @author Stephane Nicoll - * @author Madhura Bhave - */ -public class EnableConfigurationPropertiesTests { - - private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @After - public void close() { - System.clearProperty("name"); - System.clearProperty("nested.name"); - System.clearProperty("nested_name"); - } - - @Test - public void testBasicPropertiesBinding() { - this.context.register(TestConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name=foo"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(TestProperties.class)).hasSize(1); - assertThat(this.context.containsBean(TestProperties.class.getName())).isTrue(); - assertThat(this.context.getBean(TestProperties.class).name).isEqualTo("foo"); - } - - @Test - public void testSystemPropertiesBinding() { - this.context.register(TestConfiguration.class); - System.setProperty("name", "foo"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(TestProperties.class)).hasSize(1); - assertThat(this.context.getBean(TestProperties.class).name).isEqualTo("foo"); - } - - @Test - public void testNestedSystemPropertiesBinding() { - this.context.register(NestedConfiguration.class); - System.setProperty("name", "foo"); - System.setProperty("nested.name", "bar"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(NestedProperties.class)).hasSize(1); - assertThat(this.context.getBean(NestedProperties.class).name).isEqualTo("foo"); - assertThat(this.context.getBean(NestedProperties.class).nested.name) - .isEqualTo("bar"); - } - - @Test - public void testStrictPropertiesBinding() { - removeSystemProperties(); - this.context.register(StrictTestConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name=foo"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(StrictTestProperties.class)) - .hasSize(1); - assertThat(this.context.getBean(TestProperties.class).name).isEqualTo("foo"); - } - - @Test - public void testPropertiesEmbeddedBinding() { - this.context.register(EmbeddedTestConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "spring.foo.name=foo"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(EmbeddedTestProperties.class)) - .hasSize(1); - assertThat(this.context.getBean(TestProperties.class).name).isEqualTo("foo"); - } - - @Test - public void testExceptionOnValidation() { - this.context.register(ExceptionIfInvalidTestConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name:foo"); - this.thrown.expectCause(Matchers.instanceOf(BindException.class)); - this.context.refresh(); - } - - @Test - public void testNoExceptionOnValidationWithoutValidated() { - this.context.register(IgnoredIfInvalidButNotValidatedTestConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name:foo"); - this.context.refresh(); - IgnoredIfInvalidButNotValidatedTestProperties bean = this.context - .getBean(IgnoredIfInvalidButNotValidatedTestProperties.class); - assertThat(bean.getDescription()).isNull(); - } - - @Test - public void testNestedPropertiesBinding() { - this.context.register(NestedConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name=foo", "nested.name=bar"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(NestedProperties.class)).hasSize(1); - assertThat(this.context.getBean(NestedProperties.class).name).isEqualTo("foo"); - assertThat(this.context.getBean(NestedProperties.class).nested.name) - .isEqualTo("bar"); - } - - @Test - public void testBasicPropertiesBindingWithAnnotationOnBaseClass() { - this.context.register(DerivedConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name=foo"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(DerivedProperties.class)).hasSize(1); - assertThat(this.context.getBean(BaseProperties.class).name).isEqualTo("foo"); - } - - @Test - public void testArrayPropertiesBinding() { - this.context.register(TestConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name=foo", "array=1,2,3"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(TestProperties.class)).hasSize(1); - assertThat(this.context.getBean(TestProperties.class).getArray()).hasSize(3); - } - - @Test - public void testCollectionPropertiesBindingFromYamlArray() { - this.context.register(TestConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name=foo", "list[0]=1", "list[1]=2"); - this.context.refresh(); - assertThat(this.context.getBean(TestProperties.class).getList()).hasSize(2); - } - - @Test - public void testCollectionPropertiesBindingWithOver256Elements() { - this.context.register(TestConfiguration.class); - List pairs = new ArrayList<>(); - pairs.add("name:foo"); - for (int i = 0; i < 1000; i++) { - pairs.add("list[" + i + "]:" + i); - } - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - pairs.toArray(new String[] {})); - this.context.refresh(); - assertThat(this.context.getBean(TestProperties.class).getList()).hasSize(1000); - } - - @Test - public void testPropertiesBindingWithoutAnnotation() { - this.context.register(InvalidConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name:foo"); - - this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("No ConfigurationProperties annotation found"); - this.context.refresh(); - } - - @Test - public void testPropertiesBindingWithoutAnnotationValue() { - this.context.register(MoreConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name=foo"); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(MoreProperties.class)).hasSize(1); - assertThat(this.context.getBean(MoreProperties.class).name).isEqualTo("foo"); - } - - @Test - public void testPropertiesBindingWithDefaultsInXml() { - this.context.register(TestConfiguration.class, DefaultXmlConfiguration.class); - this.context.refresh(); - String[] beanNames = this.context.getBeanNamesForType(TestProperties.class); - assertThat(beanNames).as("Wrong beans").containsExactly(beanNames); - assertThat(this.context.getBean(TestProperties.class).name).isEqualTo("bar"); - } - - @Test - public void testPropertiesBindingWithDefaultsInBeanMethod() { - this.context.register(DefaultConfiguration.class); - this.context.refresh(); - String[] beanNames = this.context.getBeanNamesForType(TestProperties.class); - assertThat(beanNames).as("Wrong beans").containsExactly(beanNames); - assertThat(this.context.getBean(TestProperties.class).name).isEqualTo("bar"); - } - - @Test - public void testBindingWithTwoBeans() { - this.context.register(MoreConfiguration.class, TestConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(TestProperties.class).length) - .isEqualTo(1); - assertThat(this.context.getBeanNamesForType(MoreProperties.class).length) - .isEqualTo(1); - } - - @Test - public void testBindingWithParentContext() { - AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); - parent.register(TestConfiguration.class); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(parent, "name=parent"); - parent.refresh(); - this.context.setParent(parent); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "name=child"); - this.context.register(TestConfiguration.class, TestConsumer.class); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(TestProperties.class).length) - .isEqualTo(0); - assertThat(parent.getBeanNamesForType(TestProperties.class).length).isEqualTo(1); - assertThat(this.context.getBean(TestConsumer.class).getName()) - .isEqualTo("parent"); - parent.close(); - } - - @Test - public void testBindingOnlyParentContext() { - AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(parent, "name=foo"); - parent.register(TestConfiguration.class); - parent.refresh(); - this.context.setParent(parent); - this.context.register(TestConsumer.class); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(TestProperties.class).length) - .isEqualTo(0); - assertThat(parent.getBeanNamesForType(TestProperties.class).length).isEqualTo(1); - assertThat(this.context.getBean(TestConsumer.class).getName()).isEqualTo("foo"); - } - - @Test - public void testSimpleAutoConfig() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "external.name=foo"); - this.context.register(ExampleConfig.class); - this.context.refresh(); - assertThat(this.context.getBean(External.class).getName()).isEqualTo("foo"); - } - - @Test - public void testExplicitType() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "external.name=foo"); - this.context.register(AnotherExampleConfig.class); - this.context.refresh(); - assertThat(this.context.containsBean("external-" + External.class.getName())) - .isTrue(); - assertThat(this.context.getBean(External.class).getName()).isEqualTo("foo"); - } - - @Test - public void testMultipleExplicitTypes() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "external.name=foo", "another.name=bar"); - this.context.register(FurtherExampleConfig.class); - this.context.refresh(); - assertThat(this.context.getBean(External.class).getName()).isEqualTo("foo"); - assertThat(this.context.getBean(Another.class).getName()).isEqualTo("bar"); - } - - @Test - public void testBindingWithMapKeyWithPeriod() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "mymap.key1.key2:value12", "mymap.key3:value3"); - this.context.register(ResourceBindingPropertiesWithMap.class); - this.context.refresh(); - ResourceBindingPropertiesWithMap bean = this.context - .getBean(ResourceBindingPropertiesWithMap.class); - assertThat(bean.mymap.get("key3")).isEqualTo("value3"); - assertThat(bean.mymap.get("key1.key2")).isEqualTo("value12"); - } - - @Test - public void testAnnotatedBean() { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context, - "external.name=bar", "spam.name=foo"); - this.context.register(TestConfigurationWithAnnotatedBean.class); - this.context.refresh(); - assertThat(this.context.getBean(External.class).getName()).isEqualTo("foo"); - } - - /** - * Strict tests need a known set of properties so we remove system items which may be - * environment specific. - */ - private void removeSystemProperties() { - MutablePropertySources sources = this.context.getEnvironment() - .getPropertySources(); - sources.remove("systemProperties"); - sources.remove("systemEnvironment"); - } - - @Configuration - @EnableConfigurationProperties - public static class TestConfigurationWithAnnotatedBean { - - @Bean - @ConfigurationProperties(prefix = "spam") - public External testProperties() { - return new External(); - } - - } - - @Configuration - @EnableConfigurationProperties(TestProperties.class) - protected static class TestConfiguration { - - } - - @Configuration - @EnableConfigurationProperties(StrictTestProperties.class) - protected static class StrictTestConfiguration { - - } - - @Configuration - @EnableConfigurationProperties(EmbeddedTestProperties.class) - protected static class EmbeddedTestConfiguration { - - } - - @Configuration - @EnableConfigurationProperties(ExceptionIfInvalidTestProperties.class) - protected static class ExceptionIfInvalidTestConfiguration { - - } - - @Configuration - @EnableConfigurationProperties(IgnoredIfInvalidButNotValidatedTestProperties.class) - protected static class IgnoredIfInvalidButNotValidatedTestConfiguration { - - } - - @Configuration - @EnableConfigurationProperties(DerivedProperties.class) - protected static class DerivedConfiguration { - - } - - @Configuration - @EnableConfigurationProperties(NestedProperties.class) - protected static class NestedConfiguration { - - } - - @Configuration - protected static class DefaultConfiguration { - - @Bean - public TestProperties testProperties() { - TestProperties test = new TestProperties(); - test.setName("bar"); - return test; - } - - } - - @Configuration - @ImportResource("org/springframework/boot/context/properties/testProperties.xml") - protected static class DefaultXmlConfiguration { - - } - - @EnableConfigurationProperties - @Configuration - public static class ExampleConfig { - - @Bean - public External external() { - return new External(); - } - - } - - @EnableConfigurationProperties(External.class) - @Configuration - public static class AnotherExampleConfig { - - } - - @EnableConfigurationProperties({ External.class, Another.class }) - @Configuration - public static class FurtherExampleConfig { - - } - - @EnableConfigurationProperties({ SystemEnvVar.class }) - @Configuration - public static class SystemExampleConfig { - - } - - @ConfigurationProperties(prefix = "external") - public static class External { - - private String name; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - } - - @ConfigurationProperties(prefix = "another") - public static class Another { - - private String name; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - } - - @ConfigurationProperties(prefix = "spring_test_external") - public static class SystemEnvVar { - - private String val; - - public String getVal() { - return this.val; - } - - public void setVal(String val) { - this.val = val; - } - - } - - @Component - protected static class TestConsumer { - - @Autowired - private TestProperties properties; - - @PostConstruct - public void init() { - assertThat(this.properties).isNotNull(); - } - - public String getName() { - return this.properties.name; - } - - } - - @Configuration - @EnableConfigurationProperties(MoreProperties.class) - protected static class MoreConfiguration { - - } - - @Configuration - @EnableConfigurationProperties(InvalidConfiguration.class) - protected static class InvalidConfiguration { - - } - - @ConfigurationProperties - protected static class NestedProperties { - - private String name; - - private final Nested nested = new Nested(); - - public void setName(String name) { - this.name = name; - } - - public Nested getNested() { - return this.nested; - } - - protected static class Nested { - - private String name; - - public void setName(String name) { - this.name = name; - } - - } - - } - - @ConfigurationProperties - protected static class BaseProperties { - - private String name; - - public void setName(String name) { - this.name = name; - } - - } - - protected static class DerivedProperties extends BaseProperties { - - } - - @ConfigurationProperties - protected static class TestProperties { - - private String name; - - private int[] array; - - private List list = new ArrayList<>(); - - // No getter - you should be able to bind to a write-only bean - - public void setName(String name) { - this.name = name; - } - - public void setArray(int... values) { - this.array = values; - } - - public int[] getArray() { - return this.array; - } - - public List getList() { - return this.list; - } - - public void setList(List list) { - this.list = list; - } - - } - - @ConfigurationProperties(ignoreUnknownFields = false) - protected static class StrictTestProperties extends TestProperties { - - } - - @ConfigurationProperties(prefix = "spring.foo") - protected static class EmbeddedTestProperties extends TestProperties { - - } - - @ConfigurationProperties - @Validated - protected static class ExceptionIfInvalidTestProperties extends TestProperties { - - @NotNull - private String description; - - public String getDescription() { - return this.description; - } - - public void setDescription(String description) { - this.description = description; - } - - } - - @ConfigurationProperties - protected static class IgnoredIfInvalidButNotValidatedTestProperties - extends TestProperties { - - @NotNull - private String description; - - public String getDescription() { - return this.description; - } - - public void setDescription(String description) { - this.description = description; - } - - } - - @ConfigurationProperties - protected static class MoreProperties { - - private String name; - - public void setName(String name) { - this.name = name; - } - - // No getter - you should be able to bind to a write-only bean - - } - - // No annotation - protected static class InvalidProperties { - - private String name; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - } - - @EnableConfigurationProperties - @ConfigurationProperties - protected static class ResourceBindingPropertiesWithMap { - - private Map mymap; - - public void setMymap(Map mymap) { - this.mymap = mymap; - } - - public Map getMymap() { - return this.mymap; - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java index 1964aabbd1..3133767b34 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindableTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -24,6 +24,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.context.annotation.Bean; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; @@ -140,6 +141,20 @@ public class BindableTests { .containsExactly(annotation); } + @Test + public void getAnnotationWhenMatchShouldReuturnAnnotation() { + Test annotation = AnnotationUtils.synthesizeAnnotation(Test.class); + assertThat(Bindable.of(String.class).withAnnotations(annotation) + .getAnnotation(Test.class)).isSameAs(annotation); + } + + @Test + public void getAnnotationWhenNoMatchShouldReturnNull() { + Test annotation = AnnotationUtils.synthesizeAnnotation(Test.class); + assertThat(Bindable.of(String.class).withAnnotations(annotation) + .getAnnotation(Bean.class)).isNull(); + } + @Test public void toStringShouldShowDetails() { Annotation annotation = AnnotationUtils diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java index 5f5c3ab033..3d4a70a33c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -127,8 +127,8 @@ public class ValidationBindHandlerTests { BindValidationException cause = bindAndExpectValidationError( () -> this.binder.bind(ConfigurationPropertyName.of("foo"), Bindable.of(ExampleValidatedWithNestedBean.class), this.handler)); - assertThat(cause.getValidationErrors().getName().toString()) - .isEqualTo("foo.nested"); + assertThat(cause.getValidationErrors().getName().toString()).isEqualTo("foo"); + assertThat(cause.getMessage()).contains("nested.age"); } @Test @@ -144,11 +144,13 @@ public class ValidationBindHandlerTests { } @Test - public void bindShouldNotValidateWithoutAnnotation() { + public void bindShouldValidateWithoutAnnotation() { ExampleNonValidatedBean existingValue = new ExampleNonValidatedBean(); - this.binder.bind(ConfigurationPropertyName.of("foo"), Bindable - .of(ExampleNonValidatedBean.class).withExistingValue(existingValue), - this.handler); + bindAndExpectValidationError( + () -> this.binder.bind(ConfigurationPropertyName.of("foo"), + Bindable.of(ExampleNonValidatedBean.class) + .withExistingValue(existingValue), + this.handler)); } private BindValidationException bindAndExpectValidationError(Runnable action) { @@ -156,8 +158,6 @@ public class ValidationBindHandlerTests { action.run(); } catch (BindException ex) { - ex.printStackTrace(); - BindValidationException cause = (BindValidationException) ex.getCause(); return cause; } diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/context/properties/testProperties.xml b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/context/properties/testProperties.xml index bb8c2eb2de..a8ecd1a55a 100644 --- a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/context/properties/testProperties.xml +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/context/properties/testProperties.xml @@ -4,8 +4,8 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + id="org.springframework.boot.context.properties.ConfigurationPropertiesTests$BasicProperties" + class="org.springframework.boot.context.properties.ConfigurationPropertiesTests$BasicProperties">