From e779fb0bc40f377eace4a41fcf9311b9d9515f51 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Jun 2023 15:26:57 +0100 Subject: [PATCH] Validate constructor bound config props that implement Validator Closes gh-33669 --- .../ConfigurationPropertiesBinder.java | 35 +++++++++++++++- .../ConfigurationPropertiesTests.java | 41 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index 8566fed6b2..d74bb84fe4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -51,6 +51,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.PropertySources; +import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; @@ -136,6 +137,7 @@ class ConfigurationPropertiesBinder { : new IgnoreTopLevelConverterNotFoundBindHandler(); } + @SuppressWarnings("unchecked") private List getValidators(Bindable target) { List validators = new ArrayList<>(3); if (this.configurationPropertiesValidator != null) { @@ -144,8 +146,13 @@ class ConfigurationPropertiesBinder { if (this.jsr303Present && target.getAnnotation(Validated.class) != null) { validators.add(getJsr303Validator()); } - if (target.getValue() != null && target.getValue().get() instanceof Validator) { - validators.add((Validator) target.getValue().get()); + if (target.getValue() != null) { + if (target.getValue().get() instanceof Validator) { + validators.add((Validator) target.getValue().get()); + } + } + else if (Validator.class.isAssignableFrom(target.getType().resolve())) { + validators.add(new SelfValidatingConstructorBoundBindableValidator((Bindable) target)); } return validators; } @@ -258,4 +265,28 @@ class ConfigurationPropertiesBinder { } + /** + * A {@code Validator} for a constructor-bound {@code Bindable} where the type being + * bound is itself a {@code Validator} implementation. + */ + static class SelfValidatingConstructorBoundBindableValidator implements Validator { + + private final Bindable bindable; + + SelfValidatingConstructorBoundBindableValidator(Bindable bindable) { + this.bindable = bindable; + } + + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(this.bindable.getType().resolve()); + } + + @Override + public void validate(Object target, Errors errors) { + ((Validator) target).validate(target, errors); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index e68fa1a276..38bfb3aa3a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -731,6 +731,16 @@ class ConfigurationPropertiesTests { }); } + @Test + void loadWhenConstructorBoundConfigurationPropertiesIsAlsoValidatorShouldApplyValidator() { + assertThatExceptionOfType(Exception.class) + .isThrownBy(() -> load(ValidatorConstructorBoundPropertiesConfiguration.class)) + .satisfies((ex) -> { + assertThat(ex).hasCauseInstanceOf(BindException.class); + assertThat(ex.getCause()).hasCauseExactlyInstanceOf(BindValidationException.class); + }); + } + @Test void loadWhenConfigurationPropertiesWithValidDefaultValuesShouldNotFail() { AnnotationConfigApplicationContext context = load(ValidatorPropertiesWithDefaultValues.class); @@ -2025,6 +2035,37 @@ class ConfigurationPropertiesTests { } + @EnableConfigurationProperties(ValidatorConstructorBoundProperties.class) + static class ValidatorConstructorBoundPropertiesConfiguration { + + } + + @ConstructorBinding + @ConfigurationProperties + static class ValidatorConstructorBoundProperties implements Validator { + + private final String foo; + + ValidatorConstructorBoundProperties(String foo) { + this.foo = foo; + } + + @Override + public boolean supports(Class type) { + return type == ValidatorConstructorBoundProperties.class; + } + + @Override + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "foo", "TEST1"); + } + + String getFoo() { + return this.foo; + } + + } + @EnableConfigurationProperties @ConfigurationProperties(prefix = "test") static class WithSetterThatThrowsValidationExceptionProperties {