From be1dc647a5687ffc9175283c3f869d40fcbcf6c0 Mon Sep 17 00:00:00 2001 From: Christian Dupuis Date: Tue, 28 Jan 2014 17:02:56 +0100 Subject: [PATCH] Beans annotated with @ConfigurationProperties can be their own validators when implementing Spring's Validator interface fixes #255 --- ...urationPropertiesBindingPostProcessor.java | 51 +++++- ...onPropertiesBindingPostProcessorTests.java | 151 ++++++++++++++++++ 2 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index 75c45472aff..70ebbddaa38 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -49,8 +49,10 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; @@ -60,6 +62,7 @@ import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; * * @author Dave Syer * @author Phillip Webb + * @author Christian Dupuis */ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, BeanFactoryAware, ResourceLoaderAware, EnvironmentAware, ApplicationContextAware, @@ -294,7 +297,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc else { factory.setPropertySources(this.propertySources); } - factory.setValidator(this.validator); + factory.setValidator(determineValidator(bean)); // If no explicit conversion service is provided we add one so that (at least) // comma-separated arrays of convertibles can be bound automatically factory.setConversionService(this.conversionService == null ? getDefaultConversionService() @@ -318,6 +321,16 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc } } + private Validator determineValidator(Object bean) { + if (ClassUtils.isAssignable(Validator.class, bean.getClass())) { + if (this.validator == null) { + return (Validator) bean; + } + return new ChainingValidator(this.validator, (Validator) bean); + } + return this.validator; + } + private PropertySources loadPropertySources(String[] path) { MutablePropertySources propertySources = new MutablePropertySources(); PropertySourceLoader[] loaders = { @@ -365,4 +378,38 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc } + /** + * {@link Validator} implementation that wraps {@link Validator} instances and chains + * their execution. + */ + private static class ChainingValidator implements Validator { + + private Validator[] validators; + + public 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); + } + } + } + + } + } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java new file mode 100644 index 00000000000..f8bd7907ef8 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2014 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 javax.validation.constraints.NotNull; + +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.junit.Assert.assertTrue; + +/** + * + * Tests for {@link ConfigurationPropertiesBindingPostProcessor}. + * + * @author Christian Dupuis + */ +public class ConfigurationPropertiesBindingPostProcessorTests { + + private AnnotationConfigWebApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void testValidationWithoutJSR303() { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(TestConfigurationWithoutJSR303.class); + try { + this.context.refresh(); + } + catch (BeanCreationException ex) { + BindException bex = (BindException) ex.getRootCause(); + assertTrue(1 == bex.getErrorCount()); + } + } + + @Test + public void testValidationWithJSR303() { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(TestConfigurationWithJSR303.class); + try { + this.context.refresh(); + } + catch (BeanCreationException ex) { + BindException bex = (BindException) ex.getRootCause(); + assertTrue(2 == bex.getErrorCount()); + } + } + + @Test + public void testSuccessfulValidationWithJSR303() { + MockEnvironment env = new MockEnvironment(); + env.setProperty("test.foo", "123456"); + env.setProperty("test.bar", "654321"); + this.context = new AnnotationConfigWebApplicationContext(); + this.context.setEnvironment(env); + this.context.register(TestConfigurationWithJSR303.class); + this.context.refresh(); + } + + @Configuration + @EnableConfigurationProperties + public static class TestConfigurationWithoutJSR303 { + + @Bean + public PropertyWithoutJSR303 testProperties() { + return new PropertyWithoutJSR303(); + } + + } + + @ConfigurationProperties(name = "test") + public static class PropertyWithoutJSR303 implements Validator { + + private String foo; + + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(getClass()); + } + + @Override + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "foo", "TEST1"); + } + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + } + + @Configuration + @EnableConfigurationProperties + public static class TestConfigurationWithJSR303 { + + @Bean + public PropertyWithJSR303 testProperties() { + return new PropertyWithJSR303(); + } + + } + + @ConfigurationProperties(name = "test") + public static class PropertyWithJSR303 extends PropertyWithoutJSR303 { + + @NotNull + private String bar; + + public void setBar(String bar) { + this.bar = bar; + } + + public String getBar() { + return this.bar; + } + } + +}