Skip to content

Commit

Permalink
Introduce initializer callback for Bean Validation Configuration
Browse files Browse the repository at this point in the history
Closes gh-27956
  • Loading branch information
jhoeller committed Apr 8, 2022
1 parent 10e979e commit 35de7e1
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 34 deletions.
@@ -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.
Expand Down Expand Up @@ -31,6 +31,7 @@
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;
Expand All @@ -43,6 +44,7 @@
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;
Expand All @@ -52,18 +54,18 @@
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();

Expand All @@ -78,15 +80,15 @@ public void testSimpleValidation() {

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();
Expand All @@ -102,14 +104,15 @@ public void testSimpleValidationWithCustomProvider() {

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();

Expand All @@ -122,10 +125,13 @@ public void testSimpleValidationWithClassLevel() {
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();

Expand All @@ -135,11 +141,16 @@ public void testSpringValidationFieldType() {
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();

Expand All @@ -164,10 +175,13 @@ public void testSpringValidation() {
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();

Expand All @@ -182,10 +196,12 @@ public void testSpringValidationWithClassLevel() {
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);
Expand All @@ -202,11 +218,14 @@ public void testSpringValidationWithAutowiredValidator() {
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();

Expand All @@ -221,10 +240,13 @@ public void testSpringValidationWithErrorInListElement() {
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();

Expand All @@ -239,10 +261,13 @@ public void testSpringValidationWithErrorInSetElement() {
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();

Expand All @@ -251,10 +276,13 @@ public void testInnerBeanValidation() {
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();

Expand All @@ -263,10 +291,13 @@ public void testValidationWithOptionalField() {
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();

Expand All @@ -282,6 +313,34 @@ public void testListValidation() {
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();
}


Expand Down Expand Up @@ -380,8 +439,8 @@ public boolean isValid(ValidPerson value, ConstraintValidatorContext context) {
}
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;
}
Expand Down Expand Up @@ -417,6 +476,7 @@ public static class InnerBean {
public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}
Expand All @@ -425,8 +485,8 @@ public void setValue(String value) {

@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";

Expand All @@ -446,7 +506,8 @@ public void initialize(InnerValid constraintAnnotation) {
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;
Expand Down Expand Up @@ -494,7 +555,8 @@ public boolean isValid(List<String> list, ConstraintValidatorContext context) {
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;
}
}
Expand Down
@@ -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.
Expand All @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -113,6 +114,9 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter

private final Map<String, String> validationPropertyMap = new HashMap<>();

@Nullable
private Consumer<Configuration<?>> configurationInitializer;

@Nullable
private ApplicationContext applicationContext;

Expand Down Expand Up @@ -234,6 +238,18 @@ public Map<String, String> getValidationPropertyMap() {
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.
* <p>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<Configuration<?>> configurationInitializer) {
this.configurationInitializer = configurationInitializer;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
Expand Down Expand Up @@ -312,6 +328,9 @@ public void afterPropertiesSet() {
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 {
Expand Down

0 comments on commit 35de7e1

Please sign in to comment.