From 35de7e19ee2874d53b9078f6fed14c08c634f1ff Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 14:39:36 +0200 Subject: [PATCH] Introduce initializer callback for Bean Validation Configuration Closes gh-27956 --- .../ValidatorFactoryTests.java | 118 +++++++++++++----- .../LocalValidatorFactoryBean.java | 21 +++- .../beanvalidation/ValidatorFactoryTests.java | 41 +++++- 3 files changed, 146 insertions(+), 34 deletions(-) diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java index a383ae5f8b..ea2fb76ae8 100644 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -31,6 +31,7 @@ import java.util.Set; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintValidatorFactory; import javax.validation.ConstraintViolation; import javax.validation.Payload; import javax.validation.Valid; @@ -43,6 +44,7 @@ import org.hibernate.validator.HibernateValidatorFactory; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.convert.support.DefaultConversionService; @@ -52,18 +54,18 @@ import org.springframework.validation.Errors; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; import static org.assertj.core.api.Assertions.assertThat; /** * @author Juergen Hoeller */ -@SuppressWarnings("resource") -public class ValidatorFactoryTests { +class ValidatorFactoryTests { @Test - @SuppressWarnings("cast") - public void testSimpleValidation() { + void simpleValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -78,15 +80,15 @@ public class ValidatorFactoryTests { Validator nativeValidator = validator.unwrap(Validator.class); assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); - assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + assertThat(validator.unwrap(HibernateValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); validator.destroy(); } @Test - @SuppressWarnings("cast") - public void testSimpleValidationWithCustomProvider() { + void simpleValidationWithCustomProvider() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.setProviderClass(HibernateValidator.class); validator.afterPropertiesSet(); @@ -102,14 +104,15 @@ public class ValidatorFactoryTests { Validator nativeValidator = validator.unwrap(Validator.class); assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); - assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + assertThat(validator.unwrap(HibernateValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); validator.destroy(); } @Test - public void testSimpleValidationWithClassLevel() { + void simpleValidationWithClassLevel() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -122,10 +125,13 @@ public class ValidatorFactoryTests { ConstraintViolation cv = iterator.next(); assertThat(cv.getPropertyPath().toString()).isEqualTo(""); assertThat(cv.getConstraintDescriptor().getAnnotation() instanceof NameAddressValid).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationFieldType() { + void springValidationFieldType() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -135,11 +141,16 @@ public class ValidatorFactoryTests { BeanPropertyBindingResult errors = new BeanPropertyBindingResult(person, "person"); validator.validate(person, errors); assertThat(errors.getErrorCount()).isEqualTo(1); - assertThat(errors.getFieldError("address").getRejectedValue()).isInstanceOf(ValidAddress.class); + assertThat(errors.getFieldError("address").getRejectedValue()) + .as("Field/Value type mismatch") + .isInstanceOf(ValidAddress.class); + + validator.destroy(); } @Test - public void testSpringValidation() { + void springValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -164,10 +175,13 @@ public class ValidatorFactoryTests { assertThat(errorCodes.contains("NotNull.street")).isTrue(); assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); assertThat(errorCodes.contains("NotNull")).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationWithClassLevel() { + void springValidationWithClassLevel() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -182,10 +196,12 @@ public class ValidatorFactoryTests { assertThat(errorCodes.size()).isEqualTo(2); assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationWithAutowiredValidator() { + void springValidationWithAutowiredValidator() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext( LocalValidatorFactoryBean.class); LocalValidatorFactoryBean validator = ctx.getBean(LocalValidatorFactoryBean.class); @@ -202,11 +218,14 @@ public class ValidatorFactoryTests { assertThat(errorCodes.size()).isEqualTo(2); assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + + validator.destroy(); ctx.close(); } @Test - public void testSpringValidationWithErrorInListElement() { + void springValidationWithErrorInListElement() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -221,10 +240,13 @@ public class ValidatorFactoryTests { assertThat(fieldError.getField()).isEqualTo("address.street"); fieldError = result.getFieldError("addressList[0].street"); assertThat(fieldError.getField()).isEqualTo("addressList[0].street"); + + validator.destroy(); } @Test - public void testSpringValidationWithErrorInSetElement() { + void springValidationWithErrorInSetElement() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -239,10 +261,13 @@ public class ValidatorFactoryTests { assertThat(fieldError.getField()).isEqualTo("address.street"); fieldError = result.getFieldError("addressSet[].street"); assertThat(fieldError.getField()).isEqualTo("addressSet[].street"); + + validator.destroy(); } @Test - public void testInnerBeanValidation() { + void innerBeanValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -251,10 +276,13 @@ public class ValidatorFactoryTests { validator.validate(mainBean, errors); Object rejected = errors.getFieldValue("inner.value"); assertThat(rejected).isNull(); + + validator.destroy(); } @Test - public void testValidationWithOptionalField() { + void validationWithOptionalField() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -263,10 +291,13 @@ public class ValidatorFactoryTests { validator.validate(mainBean, errors); Object rejected = errors.getFieldValue("inner.value"); assertThat(rejected).isNull(); + + validator.destroy(); } @Test - public void testListValidation() { + void listValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -282,6 +313,34 @@ public class ValidatorFactoryTests { assertThat(fieldError).isNotNull(); assertThat(fieldError.getRejectedValue()).isEqualTo("X"); assertThat(errors.getFieldValue("list[1]")).isEqualTo("X"); + + validator.destroy(); + } + + @Test + void withConstraintValidatorFactory() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConstraintValidatorFactory(cvf); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); + } + + @Test + void withCustomInitializer() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConfigurationInitializer(configuration -> configuration.constraintValidatorFactory(cvf)); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); } @@ -380,8 +439,8 @@ public class ValidatorFactoryTests { } boolean valid = (value.name == null || !value.address.street.contains(value.name)); if (!valid && "Phil".equals(value.name)) { - context.buildConstraintViolationWithTemplate( - context.getDefaultConstraintMessageTemplate()).addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); } return valid; } @@ -417,6 +476,7 @@ public class ValidatorFactoryTests { public String getValue() { return value; } + public void setValue(String value) { this.value = value; } @@ -425,8 +485,8 @@ public class ValidatorFactoryTests { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) - @Constraint(validatedBy=InnerValidator.class) - public static @interface InnerValid { + @Constraint(validatedBy = InnerValidator.class) + public @interface InnerValid { String message() default "NOT VALID"; @@ -446,7 +506,8 @@ public class ValidatorFactoryTests { public boolean isValid(InnerBean bean, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); if (bean.getValue() == null) { - context.buildConstraintViolationWithTemplate("NULL").addPropertyNode("value").addConstraintViolation(); + context.buildConstraintViolationWithTemplate("NULL") + .addPropertyNode("value").addConstraintViolation(); return false; } return true; @@ -494,7 +555,8 @@ public class ValidatorFactoryTests { boolean valid = true; for (int i = 0; i < list.size(); i++) { if ("X".equals(list.get(i))) { - context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addBeanNode().inIterable().atIndex(i).addConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addBeanNode().inIterable().atIndex(i).addConstraintViolation(); valid = false; } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java index 73ec646bea..25fc072747 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.function.Consumer; import javax.validation.Configuration; import javax.validation.ConstraintValidatorFactory; @@ -113,6 +114,9 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter private final Map validationPropertyMap = new HashMap<>(); + @Nullable + private Consumer> configurationInitializer; + @Nullable private ApplicationContext applicationContext; @@ -234,6 +238,18 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter return this.validationPropertyMap; } + /** + * Specify a callback for customizing the Bean Validation {@code Configuration} instance, + * as an alternative to overriding the {@link #postProcessConfiguration(Configuration)} + * method in custom {@code LocalValidatorFactoryBean} subclasses. + *

This enables convenient customizations for application purposes. Infrastructure + * extensions may keep overriding the {@link #postProcessConfiguration} template method. + * @since 5.3.19 + */ + public void setConfigurationInitializer(Consumer> configurationInitializer) { + this.configurationInitializer = configurationInitializer; + } + @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; @@ -312,6 +328,9 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter this.validationPropertyMap.forEach(configuration::addProperty); // Allow for custom post-processing before we actually build the ValidatorFactory. + if (this.configurationInitializer != null) { + this.configurationInitializer.accept(configuration); + } postProcessConfiguration(configuration); try { diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java index ca7c256cd8..4b81bfe12a 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java @@ -31,6 +31,7 @@ import java.util.Set; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintValidatorFactory; import javax.validation.ConstraintViolation; import javax.validation.Payload; import javax.validation.Valid; @@ -43,6 +44,7 @@ import org.hibernate.validator.HibernateValidatorFactory; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.convert.support.DefaultConversionService; @@ -313,6 +315,32 @@ class ValidatorFactoryTests { validator.destroy(); } + @Test + void withConstraintValidatorFactory() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConstraintValidatorFactory(cvf); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); + } + + @Test + void withCustomInitializer() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConfigurationInitializer(configuration -> configuration.constraintValidatorFactory(cvf)); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); + } + @NameAddressValid public static class ValidPerson { @@ -409,8 +437,8 @@ class ValidatorFactoryTests { } boolean valid = (value.name == null || !value.address.street.contains(value.name)); if (!valid && "Phil".equals(value.name)) { - context.buildConstraintViolationWithTemplate( - context.getDefaultConstraintMessageTemplate()).addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); } return valid; } @@ -446,6 +474,7 @@ class ValidatorFactoryTests { public String getValue() { return value; } + public void setValue(String value) { this.value = value; } @@ -454,7 +483,7 @@ class ValidatorFactoryTests { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) - @Constraint(validatedBy=InnerValidator.class) + @Constraint(validatedBy = InnerValidator.class) public @interface InnerValid { String message() default "NOT VALID"; @@ -475,7 +504,8 @@ class ValidatorFactoryTests { public boolean isValid(InnerBean bean, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); if (bean.getValue() == null) { - context.buildConstraintViolationWithTemplate("NULL").addPropertyNode("value").addConstraintViolation(); + context.buildConstraintViolationWithTemplate("NULL") + .addPropertyNode("value").addConstraintViolation(); return false; } return true; @@ -523,7 +553,8 @@ class ValidatorFactoryTests { boolean valid = true; for (int i = 0; i < list.size(); i++) { if ("X".equals(list.get(i))) { - context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addBeanNode().inIterable().atIndex(i).addConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addBeanNode().inIterable().atIndex(i).addConstraintViolation(); valid = false; } }