Commit 1d83e87b authored by Phillip Webb's avatar Phillip Webb

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
parent 9e75680e
......@@ -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]
----
......
......@@ -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<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();
......
/*
* 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,26 +16,54 @@
package org.springframework.boot.context.properties;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.util.ClassUtils;
/**
* Exception thrown when a {@code @ConfigurationProperties} annotated object failed to be
* bound.
* Exception thrown when {@link ConfigurationProperties @ConfigurationProperties} binding
* fails.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 2.0.0
*/
class ConfigurationPropertiesBindingException extends RuntimeException {
public class ConfigurationPropertiesBindException extends BeanCreationException {
ConfigurationPropertiesBindingException(String message, Throwable cause) {
super(message, cause);
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;
}
/**
* Retrieve the innermost cause of this exception, if any.
* @return the innermost exception, or {@code null} if none
* Return the configuration properties annotation that triggered the binding.
* @return the configuration properties annotation
*/
public Throwable getRootCause() {
return NestedExceptionUtils.getRootCause(this);
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();
}
}
......@@ -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<PropertySource<?>> propertySources;
private final ConversionService conversionService;
private final ApplicationContext applicationContext;
private final Validator validator;
private final PropertySources propertySources;
private Iterable<ConfigurationPropertySource> configurationSources;
private final Validator configurationPropertiesValidator;
private final Binder binder;
private final Validator jsr303Validator;
ConfigurationPropertiesBinder(Iterable<PropertySource<?>> 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);
public void bind(Bindable<?> target) {
ConfigurationProperties annotation = target
.getAnnotation(ConfigurationProperties.class);
Assert.state(annotation != null, "Missing @ConfigurationProperties on " + target);
List<Validator> validators = getValidators(target);
BindHandler bindHandler = getBindHandler(annotation, validators);
getBinder().bind(annotation.prefix(), target, bindHandler);
}
catch (Exception ex) {
String message = "Could not bind properties to '"
+ ClassUtils.getShortName(target.getClass()) + "': "
+ getAnnotationDetails(annotation);
throw new ConfigurationPropertiesBindingException(message, ex);
private Validator getConfigurationPropertiesValidator(
ApplicationContext applicationContext, String validatorBeanName) {
if (applicationContext.containsBean(validatorBeanName)) {
return applicationContext.getBean(validatorBeanName, Validator.class);
}
return null;
}
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);
private List<Validator> getValidators(Bindable<?> target) {
List<Validator> 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);
}
return (Validator) bean;
if (target.getValue() != null && target.getValue().get() instanceof Validator) {
validators.add((Validator) target.getValue().get());
}
return (supportsBean ? this.validator : null);
return validators;
}
private BindHandler getBindHandler(ConfigurationProperties annotation,
Validator validator) {
List<Validator> 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 "";
}
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);
}
}
private Binder getBinder() {
if (this.binder == null) {
this.binder = new Binder(
ConfigurationPropertySources.from(this.propertySources),
new PropertySourcesPlaceholdersResolver(this.propertySources),
new ConversionServiceDeducer(this.applicationContext)
.getConversionService());
}
return this.binder;
}
}
......@@ -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<String, PropertySourcesPlaceholderConfigurer> 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);
bind(bean, beanName, annotation);
}
getBinder().bind(bean, annotation, type);
return bean;
}
catch (ConfigurationPropertiesBindingException ex) {
throw new BeanCreationException(beanName, ex.getMessage(), ex.getCause());
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);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
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 ConfigurationProperties getAnnotation(Object bean, String beanName) {
ConfigurationProperties annotation = this.beans.findFactoryAnnotation(beanName,
ConfigurationProperties.class);
private <A extends Annotation> A getAnnotation(Object bean, String beanName,
Class<A> 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;
}
}
......@@ -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);
}
}
......@@ -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) {
......
......@@ -27,125 +27,34 @@ 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}.
* Utility to deduce the {@link ConversionService} to use for configuration properties
* binding.
*
* @author Stephane Nicoll
* @author Phillip Webb
*/
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" };
class ConversionServiceDeducer {
private final ApplicationContext applicationContext;
private ConversionService conversionService;
private Validator validator;
private Iterable<PropertySource<?>> 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");
ConversionServiceDeducer(ApplicationContext applicationContext) {
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<PropertySource<?>> 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> T getOptionalBean(String name, Class<T> type) {
public ConversionService getConversionService() {
try {
return this.applicationContext.getBean(name, type);
return this.applicationContext.getBean(
ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME,
ConversionService.class);
}
catch (NoSuchBeanDefinitionException ex) {
return null;
return this.applicationContext.getAutowireCapableBeanFactory()
.createBean(Factory.class).create();
}
}
private static class ConversionServiceFactory {
private static class Factory {
private List<Converter<?, ?>> converters = Collections.emptyList();
......@@ -173,7 +82,7 @@ class ConfigurationPropertiesBinderBuilder {
this.genericConverters = converters;
}
public ConversionService createConversionService() {
public ConversionService create() {
DefaultConversionService conversionService = new DefaultConversionService();
for (Converter<?, ?> converter : this.converters) {
conversionService.addConverter(converter);
......
/*
* 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<String, Object> 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<Class<?>> getTypes(AnnotationMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(
EnableConfigurationProperties.class.getName(), false);
List<Class<?>> 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 String extractPrefix(Class<?> type) {
ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type,
ConfigurationProperties.class);
if (annotation != null) {
return annotation.prefix();
}
return "";
private List<Class<?>> 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 List<Class<?>> collectClasses(List<Object> list) {
ArrayList<Class<?>> result = new ArrayList<>();
for (Object object : list) {
for (Object value : (Object[]) object) {
if (value instanceof Class && value != void.class) {
result.add((Class<?>) value);
}
private void register(BeanDefinitionRegistry registry,
ConfigurableListableBeanFactory beanFactory, Class<?> type) {
String name = getName(type);
if (!containsBeanDefinition(beanFactory, name)) {
registerBeanDefinition(registry, name, type);
}
}
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,
private String getName(Class<?> type) {
ConfigurationProperties annotation = 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() + "'.");
}
}
}
/*
* 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<String, PropertySourcesPlaceholderConfigurer> 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);
}
}
/*
* 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<T> {
return this.annotations;
}
/**
* Return a single associated annotations that could affect binding.
* @param <A> the annotation type
* @param type annotation type
* @return the associated annotation or {@code null}
*/
@SuppressWarnings("unchecked")
public <A extends Annotation> A getAnnotation(Class<A> type) {
for (Annotation annotation : this.annotations) {
if (type.isInstance(annotation)) {
return (A) annotation;
}
}
return null;
}
@Override
public String toString() {
ToStringCreator creator = new ToStringCreator(this);
......
......@@ -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();
}
}
/*
* 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<ConfigurationProperty> boundProperties = new LinkedHashSet<>();
private final Deque<BindValidationException> 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);
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<ConfigurationProperty> 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);
}
}
......@@ -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<ObjectError> {
private FieldError convertFieldError(ConfigurationPropertyName name,
Set<ConfigurationProperty> boundProperties, FieldError error) {
if (error instanceof ObjectProvider<?>) {
if (error instanceof OriginProvider) {
return error;
}
return OriginTrackedFieldError.of(error,
......
/*
* 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;
}
}
}
/*
* 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<FooEnum> theValues;
public void setTheValue(FooEnum value) {
this.theValue = value;
}
public FooEnum getTheValue() {
return this.theValue;
}
public List<FooEnum> getTheValues() {
return this.theValues;
}
public void setTheValues(List<FooEnum> theValues) {
this.theValues = theValues;
}
}
enum FooEnum {
FOO, BAZ, BAR
}
@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<String, String> map;
public Map<String, String> getMap() {
return this.map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
@ConfigurationProperties(prefix = "test")
public static class PropertiesWithComplexMap {
private Map<String, Map<String, String>> map;
public Map<String, Map<String, String>> getMap() {
return this.map;
}
public void setMap(Map<String, Map<String, String>> map) {
this.map = map;
}
}
@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");
}
}
}
/*
* 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<String, Object> source = new HashMap<>();
source.put("com.example.bar", 5);
source.put("com.example.foo", "${com.example.bar}");
propertySources.addLast(new MapPropertySource("test", source));
this.context.register(TestConfiguration.class);
this.context.refresh();
int foo = this.context.getBean(TestConfiguration.class).getFoo();
assertThat(foo).isEqualTo(10);
}
@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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Person> personConverter() {
return new PersonConverter();
}
}
@Configuration
static class NonQualifiedConverterConfiguration {
@Bean
public Converter<String, Person> personConverter() {
return new PersonConverter();
}
}
private static class PersonConverter implements Converter<String, Person> {
@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<ConvertiblePair> 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<String, Foo> foos = new LinkedHashMap<>();
public Map<String, Foo> 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<String> aBeanToBind() {
return new AGenericClass<>();
}
}
static class AGenericClass<T> {
private T bar;
public T getBar() {
return this.bar;
}
public void setBar(T bar) {
this.bar = bar;
}
}
}
/*
* 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<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Person> personConverter() {
return new PersonConverter();
}
}
@Configuration
static class NonQualifiedConverterConfiguration {
@Bean
public Converter<String, Person> 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<String> 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<T> {
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<Integer> 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<Integer> getList() {
return this.list;
}
public void setList(List<Integer> 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<Jsr303Properties> properties = Collections
.singletonList(new Jsr303Properties());
public List<Jsr303Properties> 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<String, String> mymap;
public void setMymap(Map<String, String> mymap) {
this.mymap = mymap;
}
public Map<String, String> 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<FooEnum> theValues;
public void setTheValue(FooEnum value) {
this.theValue = value;
}
public FooEnum getTheValue() {
return this.theValue;
}
public List<FooEnum> getTheValues() {
return this.theValues;
}
public void setTheValues(List<FooEnum> theValues) {
this.theValues = theValues;
}
}
enum FooEnum {
FOO, BAZ, BAR
}
@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<String, String> map;
public Map<String, String> getMap() {
return this.map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test")
static class WithComplexMapProperties {
private Map<String, Map<String, String>> map;
public Map<String, Map<String, String>> getMap() {
return this.map;
}
public void setMap(Map<String, Map<String, String>> map) {
this.map = map;
}
}
@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<String, BasicProperties> properties = new LinkedHashMap<>();
public Map<String, BasicProperties> 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<String, Person> {
@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<ConvertiblePair> 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;
}
}
}
/*
* 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<String> 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<Integer> 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<Integer> getList() {
return this.list;
}
public void setList(List<Integer> 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<String, String> mymap;
public void setMymap(Map<String, String> mymap) {
this.mymap = mymap;
}
public Map<String, String> getMymap() {
return this.mymap;
}
}
}
/*
* 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
......
/*
* 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;
}
......
......@@ -4,8 +4,8 @@
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean
id="org.springframework.boot.context.properties.EnableConfigurationPropertiesTests$TestProperties"
class="org.springframework.boot.context.properties.EnableConfigurationPropertiesTests$TestProperties">
id="org.springframework.boot.context.properties.ConfigurationPropertiesTests$BasicProperties"
class="org.springframework.boot.context.properties.ConfigurationPropertiesTests$BasicProperties">
<property name="name" value="bar"/>
</bean>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment