Introduce functional factory methods in Validator

This commit introduces `of` method in `Validator` to provide a way to
create a validator for the specific type `<T>` using `BiConsumer<T, Errors>`
and define the validator in a functional way.
This also eliminates the boilerplate for implementing the `supports` method.
This commit is contained in:
Toshiaki Maki 2023-01-27 15:43:06 +09:00 committed by Arjen Poutsma
parent 7492c0ea03
commit 5f98afc180
3 changed files with 65 additions and 72 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2021 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,10 @@
package org.springframework.validation; package org.springframework.validation;
import java.util.function.BiConsumer;
import org.springframework.util.Assert;
/** /**
* A validator for application-specific objects. * A validator for application-specific objects.
* *
@ -59,6 +63,7 @@ package org.springframework.validation;
* application. * application.
* *
* @author Rod Johnson * @author Rod Johnson
* @author Toshiaki Maki
* @see SmartValidator * @see SmartValidator
* @see Errors * @see Errors
* @see ValidationUtils * @see ValidationUtils
@ -92,4 +97,38 @@ public interface Validator {
*/ */
void validate(Object target, Errors errors); void validate(Object target, Errors errors);
/**
* Takes the {@link BiConsumer} containing the validation logic for the specific type
* <code>&lt;T&gt;</code> and returns the {@link Validator} instance.<br>
* This validator implements the <i>typical</i> {@link #supports(Class)} method
* for the given <code>&lt;T&gt;</code>.<br>
*
* By using this method, a {@link Validator} can be implemented as follows:
*
* <pre class="code">Validator passwordEqualsValidator = Validator.of(PasswordResetForm.class, (form, errors) -> {
* if (!Objects.equals(form.getPassword(), form.getConfirmPassword())) {
* errors.rejectValue("confirmPassword",
* "PasswordEqualsValidator.passwordResetForm.password",
* "password and confirm password must be same.");
* }
* });</pre>
* @param targetClass the class of the object that is to be validated
* @param delegate the validation logic to delegate for the specific type <code>&lt;T&gt;</code>
* @param <T> the type of the object that is to be validated
* @return the {@link Validator} instance
*/
static <T> Validator of(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
Assert.notNull(targetClass, "'targetClass' must not be null.");
return new Validator() {
@Override
public boolean supports(Class<?> clazz) {
return targetClass.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
delegate.accept(targetClass.cast(target), errors);
}
};
}
} }

View File

@ -82,6 +82,16 @@ import static org.assertj.core.api.Assertions.entry;
*/ */
class DataBinderTests { class DataBinderTests {
Validator spouseValidator = Validator.of(TestBean.class, (tb, errors) -> {
if (tb == null || "XXX".equals(tb.getName())) {
errors.rejectValue("", "SPOUSE_NOT_AVAILABLE");
return;
}
if (tb.getAge() < 32) {
errors.rejectValue("age", "TOO_YOUNG", "simply too young");
}
});
@Test @Test
void bindingNoErrors() throws BindException { void bindingNoErrors() throws BindException {
TestBean rod = new TestBean(); TestBean rod = new TestBean();
@ -1144,7 +1154,6 @@ class DataBinderTests {
errors.setNestedPath("spouse"); errors.setNestedPath("spouse");
assertThat(errors.getNestedPath()).isEqualTo("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse.");
assertThat(errors.getFieldValue("age")).isEqualTo("argh"); assertThat(errors.getFieldValue("age")).isEqualTo("argh");
Validator spouseValidator = new SpouseValidator();
spouseValidator.validate(tb.getSpouse(), errors); spouseValidator.validate(tb.getSpouse(), errors);
errors.setNestedPath(""); errors.setNestedPath("");
@ -1195,7 +1204,6 @@ class DataBinderTests {
errors.setNestedPath("spouse."); errors.setNestedPath("spouse.");
assertThat(errors.getNestedPath()).isEqualTo("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse.");
Validator spouseValidator = new SpouseValidator();
spouseValidator.validate(tb.getSpouse(), errors); spouseValidator.validate(tb.getSpouse(), errors);
errors.setNestedPath(""); errors.setNestedPath("");
@ -1267,7 +1275,6 @@ class DataBinderTests {
errors.setNestedPath("spouse."); errors.setNestedPath("spouse.");
assertThat(errors.getNestedPath()).isEqualTo("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse.");
Validator spouseValidator = new SpouseValidator();
spouseValidator.validate(tb.getSpouse(), errors); spouseValidator.validate(tb.getSpouse(), errors);
errors.setNestedPath(""); errors.setNestedPath("");
@ -1332,7 +1339,6 @@ class DataBinderTests {
testValidator.validate(tb, errors); testValidator.validate(tb, errors);
errors.setNestedPath("spouse."); errors.setNestedPath("spouse.");
assertThat(errors.getNestedPath()).isEqualTo("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse.");
Validator spouseValidator = new SpouseValidator();
spouseValidator.validate(tb.getSpouse(), errors); spouseValidator.validate(tb.getSpouse(), errors);
errors.setNestedPath(""); errors.setNestedPath("");
@ -1348,7 +1354,6 @@ class DataBinderTests {
TestBean tb = new TestBean(); TestBean tb = new TestBean();
tb.setName("XXX"); tb.setName("XXX");
Errors errors = new BeanPropertyBindingResult(tb, "tb"); Errors errors = new BeanPropertyBindingResult(tb, "tb");
Validator spouseValidator = new SpouseValidator();
spouseValidator.validate(tb, errors); spouseValidator.validate(tb, errors);
assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.hasGlobalErrors()).isTrue();
@ -2160,28 +2165,6 @@ class DataBinderTests {
} }
} }
private static class SpouseValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return TestBean.class.isAssignableFrom(clazz);
}
@Override
public void validate(@Nullable Object obj, Errors errors) {
TestBean tb = (TestBean) obj;
if (tb == null || "XXX".equals(tb.getName())) {
errors.rejectValue("", "SPOUSE_NOT_AVAILABLE");
return;
}
if (tb.getAge() < 32) {
errors.rejectValue("age", "TOO_YOUNG", "simply too young");
}
}
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static class GrowingList<E> extends AbstractList<E> { private static class GrowingList<E> extends AbstractList<E> {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,7 +19,6 @@ package org.springframework.validation;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -34,6 +33,10 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/ */
public class ValidationUtilsTests { public class ValidationUtilsTests {
Validator emptyValidator = Validator.of(TestBean.class, (testBean, errors) -> ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!"));
Validator emptyOrWhitespaceValidator = Validator.of(TestBean.class, (testBean, errors) -> ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", "You must enter a name!"));
@Test @Test
public void testInvokeValidatorWithNullValidator() throws Exception { public void testInvokeValidatorWithNullValidator() throws Exception {
TestBean tb = new TestBean(); TestBean tb = new TestBean();
@ -46,14 +49,14 @@ public class ValidationUtilsTests {
public void testInvokeValidatorWithNullErrors() throws Exception { public void testInvokeValidatorWithNullErrors() throws Exception {
TestBean tb = new TestBean(); TestBean tb = new TestBean();
assertThatIllegalArgumentException().isThrownBy(() -> assertThatIllegalArgumentException().isThrownBy(() ->
ValidationUtils.invokeValidator(new EmptyValidator(), tb, null)); ValidationUtils.invokeValidator(emptyValidator, tb, null));
} }
@Test @Test
public void testInvokeValidatorSunnyDay() throws Exception { public void testInvokeValidatorSunnyDay() throws Exception {
TestBean tb = new TestBean(); TestBean tb = new TestBean();
Errors errors = new BeanPropertyBindingResult(tb, "tb"); Errors errors = new BeanPropertyBindingResult(tb, "tb");
ValidationUtils.invokeValidator(new EmptyValidator(), tb, errors); ValidationUtils.invokeValidator(emptyValidator, tb, errors);
assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.hasFieldErrors("name")).isTrue();
assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY");
} }
@ -62,15 +65,14 @@ public class ValidationUtilsTests {
public void testValidationUtilsSunnyDay() throws Exception { public void testValidationUtilsSunnyDay() throws Exception {
TestBean tb = new TestBean(""); TestBean tb = new TestBean("");
Validator testValidator = new EmptyValidator();
tb.setName(" "); tb.setName(" ");
Errors errors = new BeanPropertyBindingResult(tb, "tb"); Errors errors = new BeanPropertyBindingResult(tb, "tb");
testValidator.validate(tb, errors); emptyValidator.validate(tb, errors);
assertThat(errors.hasFieldErrors("name")).isFalse(); assertThat(errors.hasFieldErrors("name")).isFalse();
tb.setName("Roddy"); tb.setName("Roddy");
errors = new BeanPropertyBindingResult(tb, "tb"); errors = new BeanPropertyBindingResult(tb, "tb");
testValidator.validate(tb, errors); emptyValidator.validate(tb, errors);
assertThat(errors.hasFieldErrors("name")).isFalse(); assertThat(errors.hasFieldErrors("name")).isFalse();
} }
@ -78,8 +80,7 @@ public class ValidationUtilsTests {
public void testValidationUtilsNull() throws Exception { public void testValidationUtilsNull() throws Exception {
TestBean tb = new TestBean(); TestBean tb = new TestBean();
Errors errors = new BeanPropertyBindingResult(tb, "tb"); Errors errors = new BeanPropertyBindingResult(tb, "tb");
Validator testValidator = new EmptyValidator(); emptyValidator.validate(tb, errors);
testValidator.validate(tb, errors);
assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.hasFieldErrors("name")).isTrue();
assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY");
} }
@ -88,8 +89,7 @@ public class ValidationUtilsTests {
public void testValidationUtilsEmpty() throws Exception { public void testValidationUtilsEmpty() throws Exception {
TestBean tb = new TestBean(""); TestBean tb = new TestBean("");
Errors errors = new BeanPropertyBindingResult(tb, "tb"); Errors errors = new BeanPropertyBindingResult(tb, "tb");
Validator testValidator = new EmptyValidator(); emptyValidator.validate(tb, errors);
testValidator.validate(tb, errors);
assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.hasFieldErrors("name")).isTrue();
assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY");
} }
@ -115,32 +115,31 @@ public class ValidationUtilsTests {
@Test @Test
public void testValidationUtilsEmptyOrWhitespace() throws Exception { public void testValidationUtilsEmptyOrWhitespace() throws Exception {
TestBean tb = new TestBean(); TestBean tb = new TestBean();
Validator testValidator = new EmptyOrWhitespaceValidator();
// Test null // Test null
Errors errors = new BeanPropertyBindingResult(tb, "tb"); Errors errors = new BeanPropertyBindingResult(tb, "tb");
testValidator.validate(tb, errors); emptyOrWhitespaceValidator.validate(tb, errors);
assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.hasFieldErrors("name")).isTrue();
assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE");
// Test empty String // Test empty String
tb.setName(""); tb.setName("");
errors = new BeanPropertyBindingResult(tb, "tb"); errors = new BeanPropertyBindingResult(tb, "tb");
testValidator.validate(tb, errors); emptyOrWhitespaceValidator.validate(tb, errors);
assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.hasFieldErrors("name")).isTrue();
assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE");
// Test whitespace String // Test whitespace String
tb.setName(" "); tb.setName(" ");
errors = new BeanPropertyBindingResult(tb, "tb"); errors = new BeanPropertyBindingResult(tb, "tb");
testValidator.validate(tb, errors); emptyOrWhitespaceValidator.validate(tb, errors);
assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.hasFieldErrors("name")).isTrue();
assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE");
// Test OK // Test OK
tb.setName("Roddy"); tb.setName("Roddy");
errors = new BeanPropertyBindingResult(tb, "tb"); errors = new BeanPropertyBindingResult(tb, "tb");
testValidator.validate(tb, errors); emptyOrWhitespaceValidator.validate(tb, errors);
assertThat(errors.hasFieldErrors("name")).isFalse(); assertThat(errors.hasFieldErrors("name")).isFalse();
} }
@ -163,32 +162,4 @@ public class ValidationUtilsTests {
assertThat(errors.getFieldError("name").getDefaultMessage()).isEqualTo("msg"); assertThat(errors.getFieldError("name").getDefaultMessage()).isEqualTo("msg");
} }
private static class EmptyValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return TestBean.class.isAssignableFrom(clazz);
}
@Override
public void validate(@Nullable Object obj, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!");
}
}
private static class EmptyOrWhitespaceValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return TestBean.class.isAssignableFrom(clazz);
}
@Override
public void validate(@Nullable Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", "You must enter a name!");
}
}
} }