diff --git a/spring-context/src/main/java/org/springframework/validation/TypedValidator.java b/spring-context/src/main/java/org/springframework/validation/TypedValidator.java new file mode 100644 index 0000000000..000e9389b1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/TypedValidator.java @@ -0,0 +1,63 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation; + +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +import org.springframework.util.Assert; + +/** + * Validator instance returned by {@link Validator#forInstanceOf(Class, BiConsumer)} + * and {@link Validator#forType(Class, BiConsumer)}. + * + * @author Toshiaki Maki + * @author Arjen Poutsma + * @since 6.1 + * @param the target object type + */ +final class TypedValidator implements Validator { + + private final Class targetClass; + + private final Predicate> supports; + + private final BiConsumer validate; + + + public TypedValidator(Class targetClass, Predicate> supports, BiConsumer validate) { + Assert.notNull(targetClass, "TargetClass must not be null"); + Assert.notNull(supports, "Supports function must not be null"); + Assert.notNull(validate, "Validate function must not be null"); + + this.targetClass = targetClass; + this.supports = supports; + this.validate = validate; + } + + + @Override + public boolean supports(Class clazz) { + return this.supports.test(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + this.validate.accept(this.targetClass.cast(target), errors); + } + +} 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 b67b6d5d8b..36ef7b3726 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,8 @@ package org.springframework.validation; +import java.util.function.BiConsumer; + /** * A validator for application-specific objects. * @@ -26,39 +28,33 @@ package org.springframework.validation; * of an application, and supports the encapsulation of validation * logic as a first-class citizen in its own right. * - *

Find below a simple but complete {@code Validator} - * implementation, which validates that the various {@link String} - * properties of a {@code UserLogin} instance are not empty - * (that is they are not {@code null} and do not consist + *

Implementations can be created via the static factory methods + * {@link #forInstanceOf(Class, BiConsumer)} or + * {@link #forType(Class, BiConsumer)}. + * Below is a simple but complete {@code Validator} that validates that the + * various {@link String} properties of a {@code UserLogin} instance are not + * empty (they are not {@code null} and do not consist * wholly of whitespace), and that any password that is present is * at least {@code 'MINIMUM_PASSWORD_LENGTH'} characters in length. * - *

public class UserLoginValidator implements Validator {
- *
- *    private static final int MINIMUM_PASSWORD_LENGTH = 6;
- *
- *    public boolean supports(Class clazz) {
- *       return UserLogin.class.isAssignableFrom(clazz);
- *    }
- *
- *    public void validate(Object target, Errors errors) {
- *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
- *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
- *       UserLogin login = (UserLogin) target;
- *       if (login.getPassword() != null
- *             && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
- *          errors.rejectValue("password", "field.min.length",
- *                new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
- *                "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
- *       }
- *    }
- * }
+ *
Validator userLoginValidator = Validator.forInstance(UserLogin.class, (login, errors) -> {
+ *   ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
+ *   ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
+ *   if (login.getPassword() != null
+ *         && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
+ *      errors.rejectValue("password", "field.min.length",
+ *            new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
+ *            "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
+ *   }
+ * });
* *

See also the Spring reference manual for a fuller discussion of * the {@code Validator} interface and its role in an enterprise * application. * * @author Rod Johnson + * @author Toshiaki Maki + * @author Arjen Poutsma * @see SmartValidator * @see Errors * @see ValidationUtils @@ -92,4 +88,54 @@ public interface Validator { */ void validate(Object target, Errors errors); + + /** + * Return a {@code Validator} that checks whether the target object + * {@linkplain Class#isAssignableFrom(Class) is an instance of} + * {@code targetClass}, resorting to {@code delegate} to populate + * {@link Errors} if it is. + * + *

For instance: + *

Validator passwordEqualsValidator = Validator.forInstanceOf(PasswordResetForm.class, (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 supported by the returned validator + * @param delegate function invoked with the target object, if it is an + * instance of type T + * @param the target object type + * @return the created {@code Validator} + * @since 6.1 + */ + static Validator forInstanceOf(Class targetClass, BiConsumer delegate) { + return new TypedValidator<>(targetClass, targetClass::isAssignableFrom, delegate); + } + + /** + * Return a {@code Validator} that checks whether the target object's class + * is identical to {@code targetClass}, resorting to {@code delegate} to + * populate {@link Errors} if it is. + * + *

For instance: + *

Validator passwordEqualsValidator = Validator.forType(PasswordResetForm.class, (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 exact class supported by the returned validator (no subclasses) + * @param delegate function invoked with the target object, if it is an + * instance of type T + * @param the target object type + * @return the created {@code Validator} + * @since 6.1 + */ + static Validator forType(Class targetClass, BiConsumer delegate) { + return new TypedValidator<>(targetClass, targetClass::equals, delegate); + } + } 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 a990612917..e4c4ae97c1 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -82,6 +82,16 @@ import static org.assertj.core.api.Assertions.entry; */ class DataBinderTests { + private final Validator spouseValidator = Validator.forInstanceOf(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 void bindingNoErrors() throws BindException { TestBean rod = new TestBean(); @@ -1144,7 +1154,6 @@ class DataBinderTests { 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 +1204,6 @@ class DataBinderTests { errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1267,7 +1275,6 @@ class DataBinderTests { errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); - Validator spouseValidator = new SpouseValidator(); spouseValidator.validate(tb.getSpouse(), errors); errors.setNestedPath(""); @@ -1332,7 +1339,6 @@ class DataBinderTests { 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 +1354,6 @@ class DataBinderTests { 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 +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") 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 0a027b95df..09694c8e8b 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. @@ -19,7 +19,6 @@ package org.springframework.validation; 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; @@ -34,6 +33,10 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException */ public class ValidationUtilsTests { + private final Validator emptyValidator = Validator.forInstanceOf(TestBean.class, (testBean, errors) -> ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!")); + + private final Validator emptyOrWhitespaceValidator = Validator.forInstanceOf(TestBean.class, (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 +49,14 @@ public class ValidationUtilsTests { 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 +65,14 @@ public class ValidationUtilsTests { 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 +80,7 @@ public class ValidationUtilsTests { 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 +89,7 @@ public class ValidationUtilsTests { 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 +115,31 @@ public class ValidationUtilsTests { @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 +162,4 @@ public class ValidationUtilsTests { 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!"); - } - } - } diff --git a/spring-context/src/test/java/org/springframework/validation/ValidatorTests.java b/spring-context/src/test/java/org/springframework/validation/ValidatorTests.java new file mode 100644 index 0000000000..0827b85f85 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/ValidatorTests.java @@ -0,0 +1,48 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class ValidatorTests { + + @Test + public void testSupportsForInstanceOf() { + Validator validator = Validator.forInstanceOf(TestBean.class, (testBean, errors) -> {}); + assertThat(validator.supports(TestBean.class)).isTrue(); + assertThat(validator.supports(TestBeanSubclass.class)).isTrue(); + } + + @Test + public void testSupportsForType() { + Validator validator = Validator.forType(TestBean.class, (testBean, errors) -> {}); + assertThat(validator.supports(TestBean.class)).isTrue(); + assertThat(validator.supports(TestBeanSubclass.class)).isFalse(); + } + + + private static class TestBeanSubclass extends TestBean { + } + +}