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 ApplicationContext applicationContext;
private final ConversionService conversionService;
private final PropertySources propertySources;
private final Validator validator;
private final Validator configurationPropertiesValidator;
private Iterable<ConfigurationPropertySource> configurationSources;
private final Validator jsr303Validator;
private final Binder binder;
private volatile Binder binder;
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);
ConfigurationPropertiesBinder(ApplicationContext applicationContext,
String validatorBeanName) {
this.applicationContext = applicationContext;
this.propertySources = new PropertySourcesDeducer(applicationContext)
.getPropertySources();
this.configurationPropertiesValidator = getConfigurationPropertiesValidator(
applicationContext, validatorBeanName);
this.jsr303Validator = ConfigurationPropertiesJsr303Validator
.getIfJsr303Present(applicationContext);
}
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);
}
/**
* Bind the specified {@code target} object using the configuration defined by the
* specified {@code annotation}.
* @param target the target to bind the configuration property sources to
* @param annotation the binding configuration
* @param targetType the resolvable type for the target
* @throws ConfigurationPropertiesBindingException if the binding failed
*/
void bind(Object target, ConfigurationProperties annotation,
ResolvableType targetType) {
Validator validator = determineValidator(target);
BindHandler handler = getBindHandler(annotation, validator);
Bindable<?> bindable = Bindable.of(targetType).withExistingValue(target);
try {
this.binder.bind(annotation.prefix(), bindable, handler);
}
catch (Exception ex) {
String message = "Could not bind properties to '"
+ ClassUtils.getShortName(target.getClass()) + "': "
+ getAnnotationDetails(annotation);
throw new ConfigurationPropertiesBindingException(message, ex);
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);
}
return (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 (supportsBean ? this.validator : null);
if (target.getValue() != null && target.getValue().get() instanceof Validator) {
validators.add((Validator) target.getValue().get());
}
return validators;
}
private BindHandler getBindHandler(ConfigurationProperties annotation,
Validator validator) {
List<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 "";
private Binder getBinder() {
if (this.binder == null) {
this.binder = new Binder(
ConfigurationPropertySources.from(this.propertySources),
new PropertySourcesPlaceholdersResolver(this.propertySources),
new ConversionServiceDeducer(this.applicationContext)
.getConversionService());
}
StringBuilder details = new StringBuilder();
details.append("prefix=").append(annotation.prefix());
details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields());
details.append(", ignoreUnknownFields=").append(annotation.ignoreUnknownFields());
return details.toString();
}
/**
* {@link Validator} implementation that wraps {@link Validator} instances and chains
* their execution.
*/
private static class ChainingValidator implements Validator {
private final Validator[] validators;
ChainingValidator(Validator... validators) {
Assert.notNull(validators, "Validators must not be null");
this.validators = validators;
}
@Override
public boolean supports(Class<?> clazz) {
for (Validator validator : this.validators) {
if (validator.supports(clazz)) {
return true;
}
}
return false;
}
@Override
public void validate(Object target, Errors errors) {
for (Validator validator : this.validators) {
if (validator.supports(target.getClass())) {
validator.validate(target, errors);
}
}
}
return this.binder;
}
}
......@@ -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();
}
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);
// 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 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);
}
getBinder().bind(bean, annotation, type);
}
catch (ConfigurationPropertiesBindingException ex) {
throw new BeanCreationException(beanName, ex.getMessage(), ex.getCause());
}
bind(bean, beanName, annotation);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
ResolvableType type = getBeanType(bean, beanName);
Validated validated = getAnnotation(bean, beanName, Validated.class);
Annotation[] annotations = (validated == null ? new Annotation[] { annotation }
: new Annotation[] { annotation, validated });
Bindable<?> target = Bindable.of(type).withExistingValue(bean)
.withAnnotations(annotations);
try {
this.configurationPropertiesBinder.bind(target);
}
catch (Exception ex) {
throw new ConfigurationPropertiesBindException(beanName, bean, annotation,
ex);
}
}
private ConfigurationProperties getAnnotation(Object bean, String beanName) {
ConfigurationProperties annotation = this.beans.findFactoryAnnotation(beanName,
ConfigurationProperties.class);
if (annotation == null) {
annotation = AnnotationUtils.findAnnotation(bean.getClass(),
ConfigurationProperties.class);
private ResolvableType getBeanType(Object bean, String beanName) {
Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName);
if (factoryMethod != null) {
return ResolvableType.forMethodReturnType(factoryMethod);
}
return annotation;
return ResolvableType.forClass(bean.getClass());
}
private ConfigurationPropertiesBinder getBinder() {
if (this.configurationPropertiesBinder == null) {
this.configurationPropertiesBinder = new ConfigurationPropertiesBinderBuilder(
this.applicationContext).withPropertySources(this.propertySources)
.build();
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(), type);
}
return this.configurationPropertiesBinder;
return annotation;
}
}
......@@ -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);
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-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