From ce65b27085e5b0c8f526d528f83da308fcf58eab Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Fri, 27 Jan 2023 15:43:06 +0900 Subject: [PATCH 1/3] Support BiConsumer to create a specific type of Validator This commit introduces `of` method in `Validator` to provide a way to create a validator for the specific type `` using `BiConsumer` and define the validator in a functional way. This also eliminates the boilerplate for implementing the `supports` method. --- .../springframework/validation/Validator.java | 44 +++++++++++++- .../validation/DataBinderTests.java | 40 ++++--------- .../validation/ValidationUtilsTests.java | 60 ++++++------------- 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java index b67b6d5d8b77..dec52b5f9a09 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -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"); * you may not use this file except in compliance with the License. @@ -16,6 +16,11 @@ package org.springframework.validation; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.springframework.util.Assert; + /** * A validator for application-specific objects. * @@ -59,6 +64,7 @@ * application. * * @author Rod Johnson + * @author Toshiaki Maki * @see SmartValidator * @see Errors * @see ValidationUtils @@ -92,4 +98,40 @@ public interface Validator { */ void validate(Object target, Errors errors); + /** + * Returns the {@link Function} that takes the {@link BiConsumer} containing validation logic for the specific type + * <T> and returns the {@link Validator} instance.
+ * This validator implements the typical {@link #supports(Class)} method + * for the given <T>.
+ * + * By using this {@link #of(Class)} method, a {@link Validator} can be implemented as follows: + * + *
Validator passwordEqualsValidator = Validator.of(PasswordResetForm.class)
+	 *     .apply((form, errors) -> {
+	 *       if (!Objects.equals(form.getPassword(), form.getConfirmPassword())) {
+	 *         errors.rejectValue("confirmPassword",
+	 *             "PasswordEqualsValidator.passwordResetForm.password",
+	 *             "password and confirm password must be same.");
+	 *       }
+	 *     });
+ * + * @param targetClass the class of the object that is to be validated + * @return The {@link Function} that takes the {@link BiConsumer} containing the validation logic and + * returns the {@link Validator} instance + * @param the type of the object that is to be validated + */ + static Function, Validator> of(Class targetClass) { + Assert.notNull(targetClass, "'targetClass' must not be null."); + return validator -> new Validator() { + @Override + public boolean supports(Class clazz) { + return targetClass.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + validator.accept(targetClass.cast(target), errors); + } + }; + } } diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 05dd886da828..3938b0591d14 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -82,6 +82,17 @@ */ class DataBinderTests { + Validator spouseValidator = Validator.of(TestBean.class) + .apply((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 void bindingNoErrors() throws BindException { TestBean rod = new TestBean(); @@ -1144,7 +1155,6 @@ void validatorNoErrors() throws Exception { errors.setNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); assertThat(errors.getFieldValue("age")).isEqualTo("argh"); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1195,7 +1205,6 @@ void validatorWithErrors() { errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1267,7 +1276,6 @@ void validatorWithErrorsAndCodesPrefix() { errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1332,7 +1340,6 @@ void validatorWithNestedObjectNull() { testValidator.validate(tb, errors); errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1348,7 +1355,6 @@ void nestedValidatorWithoutNestedPath() { TestBean tb = new TestBean(); tb.setName("XXX"); Errors errors = new BeanPropertyBindingResult(tb, "tb"); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb, errors); assertThat(errors.hasGlobalErrors()).isTrue(); @@ -2160,28 +2166,6 @@ public void validate(@Nullable Object obj, Errors errors) { } } - - 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") private static class GrowingList extends AbstractList { diff --git a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java index 0a027b95df43..ffdb6a01cae4 100644 --- a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -34,6 +34,12 @@ */ public class ValidationUtilsTests { + Validator emptyValidator = Validator.of(TestBean.class) + .apply((testBean, errors) -> ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!")); + + Validator emptyOrWhitespaceValidator = Validator.of(TestBean.class) + .apply((testBean, errors) -> ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", "You must enter a name!")); + @Test public void testInvokeValidatorWithNullValidator() throws Exception { TestBean tb = new TestBean(); @@ -46,14 +52,14 @@ public void testInvokeValidatorWithNullValidator() throws Exception { public void testInvokeValidatorWithNullErrors() throws Exception { TestBean tb = new TestBean(); assertThatIllegalArgumentException().isThrownBy(() -> - ValidationUtils.invokeValidator(new EmptyValidator(), tb, null)); + ValidationUtils.invokeValidator(emptyValidator, tb, null)); } @Test public void testInvokeValidatorSunnyDay() throws Exception { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); - ValidationUtils.invokeValidator(new EmptyValidator(), tb, errors); + ValidationUtils.invokeValidator(emptyValidator, tb, errors); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); } @@ -62,15 +68,14 @@ public void testInvokeValidatorSunnyDay() throws Exception { public void testValidationUtilsSunnyDay() throws Exception { TestBean tb = new TestBean(""); - Validator testValidator = new EmptyValidator(); tb.setName(" "); Errors errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + emptyValidator.validate(tb, errors); assertThat(errors.hasFieldErrors("name")).isFalse(); tb.setName("Roddy"); errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + emptyValidator.validate(tb, errors); assertThat(errors.hasFieldErrors("name")).isFalse(); } @@ -78,8 +83,7 @@ public void testValidationUtilsSunnyDay() throws Exception { public void testValidationUtilsNull() throws Exception { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); - Validator testValidator = new EmptyValidator(); - testValidator.validate(tb, errors); + emptyValidator.validate(tb, errors); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); } @@ -88,8 +92,7 @@ public void testValidationUtilsNull() throws Exception { public void testValidationUtilsEmpty() throws Exception { TestBean tb = new TestBean(""); Errors errors = new BeanPropertyBindingResult(tb, "tb"); - Validator testValidator = new EmptyValidator(); - testValidator.validate(tb, errors); + emptyValidator.validate(tb, errors); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); } @@ -115,32 +118,31 @@ public void testValidationUtilsEmptyVariants() { @Test public void testValidationUtilsEmptyOrWhitespace() throws Exception { TestBean tb = new TestBean(); - Validator testValidator = new EmptyOrWhitespaceValidator(); // Test null Errors errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + emptyOrWhitespaceValidator.validate(tb, errors); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test empty String tb.setName(""); errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + emptyOrWhitespaceValidator.validate(tb, errors); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test whitespace String tb.setName(" "); errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + emptyOrWhitespaceValidator.validate(tb, errors); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test OK tb.setName("Roddy"); errors = new BeanPropertyBindingResult(tb, "tb"); - testValidator.validate(tb, errors); + emptyOrWhitespaceValidator.validate(tb, errors); assertThat(errors.hasFieldErrors("name")).isFalse(); } @@ -163,32 +165,4 @@ public void testValidationUtilsEmptyOrWhitespaceVariants() { 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!"); - } - } - } From 5651a95d8968be77cda556b94b5080a5c3122a0c Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Fri, 27 Jan 2023 20:11:57 +0900 Subject: [PATCH 2/3] Fix checkstyle errors --- .../main/java/org/springframework/validation/Validator.java | 5 ++--- .../org/springframework/validation/ValidationUtilsTests.java | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java index dec52b5f9a09..f391df3afaef 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -114,11 +114,10 @@ public interface Validator { * "password and confirm password must be same."); * } * }); - * * @param targetClass the class of the object that is to be validated - * @return The {@link Function} that takes the {@link BiConsumer} containing the validation logic and - * returns the {@link Validator} instance * @param the type of the object that is to be validated + * @return the {@link Function} that takes the {@link BiConsumer} containing the validation logic and + * returns the {@link Validator} instance */ static Function, Validator> of(Class targetClass) { Assert.notNull(targetClass, "'targetClass' must not be null."); diff --git a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java index ffdb6a01cae4..b9a923b37e88 100644 --- a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -19,7 +19,6 @@ import org.junit.jupiter.api.Test; 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.assertThatIllegalArgumentException; From d4edea3e1e2e687452d5b86b0f62de951d3d4c2e Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Wed, 8 Mar 2023 14:22:05 +0900 Subject: [PATCH 3/3] Make the `BiConsumer` a parameter of the `of` method and make the `of` method return a `Validator` --- .../springframework/validation/Validator.java | 18 ++++++++---------- .../validation/DataBinderTests.java | 3 +-- .../validation/ValidationUtilsTests.java | 6 ++---- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java index f391df3afaef..3ad6ade1b550 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -17,7 +17,6 @@ package org.springframework.validation; import java.util.function.BiConsumer; -import java.util.function.Function; import org.springframework.util.Assert; @@ -99,15 +98,14 @@ public interface Validator { void validate(Object target, Errors errors); /** - * Returns the {@link Function} that takes the {@link BiConsumer} containing validation logic for the specific type + * Takes the {@link BiConsumer} containing the validation logic for the specific type * <T> and returns the {@link Validator} instance.
* This validator implements the typical {@link #supports(Class)} method * for the given <T>.
* - * By using this {@link #of(Class)} method, a {@link Validator} can be implemented as follows: + * By using this method, a {@link Validator} can be implemented as follows: * - *
Validator passwordEqualsValidator = Validator.of(PasswordResetForm.class)
-	 *     .apply((form, errors) -> {
+	 * 
Validator passwordEqualsValidator = Validator.of(PasswordResetForm.class, (form, errors) -> {
 	 *       if (!Objects.equals(form.getPassword(), form.getConfirmPassword())) {
 	 *         errors.rejectValue("confirmPassword",
 	 *             "PasswordEqualsValidator.passwordResetForm.password",
@@ -115,13 +113,13 @@ public interface Validator {
 	 *       }
 	 *     });
* @param targetClass the class of the object that is to be validated + * @param delegate the validation logic to delegate for the specific type <T> * @param the type of the object that is to be validated - * @return the {@link Function} that takes the {@link BiConsumer} containing the validation logic and - * returns the {@link Validator} instance + * @return the {@link Validator} instance */ - static Function, Validator> of(Class targetClass) { + static Validator of(Class targetClass, BiConsumer delegate) { Assert.notNull(targetClass, "'targetClass' must not be null."); - return validator -> new Validator() { + return new Validator() { @Override public boolean supports(Class clazz) { return targetClass.isAssignableFrom(clazz); @@ -129,7 +127,7 @@ public boolean supports(Class clazz) { @Override public void validate(Object target, Errors errors) { - validator.accept(targetClass.cast(target), errors); + delegate.accept(targetClass.cast(target), errors); } }; } diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 3938b0591d14..ccf887c87a45 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -82,8 +82,7 @@ */ class DataBinderTests { - Validator spouseValidator = Validator.of(TestBean.class) - .apply((tb, errors) -> { + Validator spouseValidator = Validator.of(TestBean.class, (tb, errors) -> { if (tb == null || "XXX".equals(tb.getName())) { errors.rejectValue("", "SPOUSE_NOT_AVAILABLE"); return; diff --git a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java index b9a923b37e88..bec3ff28ef27 100644 --- a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -33,11 +33,9 @@ */ public class ValidationUtilsTests { - Validator emptyValidator = Validator.of(TestBean.class) - .apply((testBean, errors) -> ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!")); + Validator emptyValidator = Validator.of(TestBean.class, (testBean, errors) -> ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!")); - Validator emptyOrWhitespaceValidator = Validator.of(TestBean.class) - .apply((testBean, errors) -> ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", "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 public void testInvokeValidatorWithNullValidator() throws Exception {