Skip to content

Commit dd9f03d

Browse files
committed
Merge pull request #29890 from making:support-biconsumer-validator
* gh-29890: Polish contribution Introduce functional factory methods in Validator
2 parents 7492c0e + 3c57d55 commit dd9f03d

File tree

5 files changed

+207
-96
lines changed

5 files changed

+207
-96
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.validation;
18+
19+
import java.util.function.BiConsumer;
20+
import java.util.function.Predicate;
21+
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* Validator instance returned by {@link Validator#forInstanceOf(Class, BiConsumer)}
26+
* and {@link Validator#forType(Class, BiConsumer)}.
27+
*
28+
* @author Toshiaki Maki
29+
* @author Arjen Poutsma
30+
* @since 6.1
31+
* @param <T> the target object type
32+
*/
33+
final class TypedValidator<T> implements Validator {
34+
35+
private final Class<T> targetClass;
36+
37+
private final Predicate<Class<?>> supports;
38+
39+
private final BiConsumer<T, Errors> validate;
40+
41+
42+
public TypedValidator(Class<T> targetClass, Predicate<Class<?>> supports, BiConsumer<T, Errors> validate) {
43+
Assert.notNull(targetClass, "TargetClass must not be null");
44+
Assert.notNull(supports, "Supports function must not be null");
45+
Assert.notNull(validate, "Validate function must not be null");
46+
47+
this.targetClass = targetClass;
48+
this.supports = supports;
49+
this.validate = validate;
50+
}
51+
52+
53+
@Override
54+
public boolean supports(Class<?> clazz) {
55+
return this.supports.test(clazz);
56+
}
57+
58+
@Override
59+
public void validate(Object target, Errors errors) {
60+
this.validate.accept(this.targetClass.cast(target), errors);
61+
}
62+
63+
}

spring-context/src/main/java/org/springframework/validation/Validator.java

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.validation;
1818

19+
import java.util.function.BiConsumer;
20+
1921
/**
2022
* A validator for application-specific objects.
2123
*
@@ -26,39 +28,33 @@
2628
* of an application, and supports the encapsulation of validation
2729
* logic as a first-class citizen in its own right.
2830
*
29-
* <p>Find below a simple but complete {@code Validator}
30-
* implementation, which validates that the various {@link String}
31-
* properties of a {@code UserLogin} instance are not empty
32-
* (that is they are not {@code null} and do not consist
31+
* <p>Implementations can be created via the static factory methods
32+
* {@link #forInstanceOf(Class, BiConsumer)} or
33+
* {@link #forType(Class, BiConsumer)}.
34+
* Below is a simple but complete {@code Validator} that validates that the
35+
* various {@link String} properties of a {@code UserLogin} instance are not
36+
* empty (they are not {@code null} and do not consist
3337
* wholly of whitespace), and that any password that is present is
3438
* at least {@code 'MINIMUM_PASSWORD_LENGTH'} characters in length.
3539
*
36-
* <pre class="code">public class UserLoginValidator implements Validator {
37-
*
38-
* private static final int MINIMUM_PASSWORD_LENGTH = 6;
39-
*
40-
* public boolean supports(Class clazz) {
41-
* return UserLogin.class.isAssignableFrom(clazz);
42-
* }
43-
*
44-
* public void validate(Object target, Errors errors) {
45-
* ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
46-
* ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
47-
* UserLogin login = (UserLogin) target;
48-
* if (login.getPassword() != null
49-
* &amp;&amp; login.getPassword().trim().length() &lt; MINIMUM_PASSWORD_LENGTH) {
50-
* errors.rejectValue("password", "field.min.length",
51-
* new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
52-
* "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
53-
* }
54-
* }
55-
* }</pre>
40+
* <pre class="code">Validator userLoginValidator = Validator.forInstance(UserLogin.class, (login, errors) -> {
41+
* ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
42+
* ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
43+
* if (login.getPassword() != null
44+
* &amp;&amp; login.getPassword().trim().length() &lt; MINIMUM_PASSWORD_LENGTH) {
45+
* errors.rejectValue("password", "field.min.length",
46+
* new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
47+
* "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
48+
* }
49+
* });</pre>
5650
*
5751
* <p>See also the Spring reference manual for a fuller discussion of
5852
* the {@code Validator} interface and its role in an enterprise
5953
* application.
6054
*
6155
* @author Rod Johnson
56+
* @author Toshiaki Maki
57+
* @author Arjen Poutsma
6258
* @see SmartValidator
6359
* @see Errors
6460
* @see ValidationUtils
@@ -92,4 +88,54 @@ public interface Validator {
9288
*/
9389
void validate(Object target, Errors errors);
9490

91+
92+
/**
93+
* Return a {@code Validator} that checks whether the target object
94+
* {@linkplain Class#isAssignableFrom(Class) is an instance of}
95+
* {@code targetClass}, resorting to {@code delegate} to populate
96+
* {@link Errors} if it is.
97+
*
98+
* <p>For instance:
99+
* <pre class="code">Validator passwordEqualsValidator = Validator.forInstanceOf(PasswordResetForm.class, (form, errors) -> {
100+
* if (!Objects.equals(form.getPassword(), form.getConfirmPassword())) {
101+
* errors.rejectValue("confirmPassword",
102+
* "PasswordEqualsValidator.passwordResetForm.password",
103+
* "password and confirm password must be same.");
104+
* }
105+
* });</pre>
106+
* @param targetClass the class supported by the returned validator
107+
* @param delegate function invoked with the target object, if it is an
108+
* instance of type T
109+
* @param <T> the target object type
110+
* @return the created {@code Validator}
111+
* @since 6.1
112+
*/
113+
static <T> Validator forInstanceOf(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
114+
return new TypedValidator<>(targetClass, targetClass::isAssignableFrom, delegate);
115+
}
116+
117+
/**
118+
* Return a {@code Validator} that checks whether the target object's class
119+
* is identical to {@code targetClass}, resorting to {@code delegate} to
120+
* populate {@link Errors} if it is.
121+
*
122+
* <p>For instance:
123+
* <pre class="code">Validator passwordEqualsValidator = Validator.forType(PasswordResetForm.class, (form, errors) -> {
124+
* if (!Objects.equals(form.getPassword(), form.getConfirmPassword())) {
125+
* errors.rejectValue("confirmPassword",
126+
* "PasswordEqualsValidator.passwordResetForm.password",
127+
* "password and confirm password must be same.");
128+
* }
129+
* });</pre>
130+
* @param targetClass the exact class supported by the returned validator (no subclasses)
131+
* @param delegate function invoked with the target object, if it is an
132+
* instance of type T
133+
* @param <T> the target object type
134+
* @return the created {@code Validator}
135+
* @since 6.1
136+
*/
137+
static <T> Validator forType(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
138+
return new TypedValidator<>(targetClass, targetClass::equals, delegate);
139+
}
140+
95141
}

spring-context/src/test/java/org/springframework/validation/DataBinderTests.java

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@
8282
*/
8383
class DataBinderTests {
8484

85+
private final Validator spouseValidator = Validator.forInstanceOf(TestBean.class, (tb, errors) -> {
86+
if (tb == null || "XXX".equals(tb.getName())) {
87+
errors.rejectValue("", "SPOUSE_NOT_AVAILABLE");
88+
return;
89+
}
90+
if (tb.getAge() < 32) {
91+
errors.rejectValue("age", "TOO_YOUNG", "simply too young");
92+
}
93+
});
94+
8595
@Test
8696
void bindingNoErrors() throws BindException {
8797
TestBean rod = new TestBean();
@@ -1144,7 +1154,6 @@ void validatorNoErrors() throws Exception {
11441154
errors.setNestedPath("spouse");
11451155
assertThat(errors.getNestedPath()).isEqualTo("spouse.");
11461156
assertThat(errors.getFieldValue("age")).isEqualTo("argh");
1147-
Validator spouseValidator = new SpouseValidator();
11481157
spouseValidator.validate(tb.getSpouse(), errors);
11491158

11501159
errors.setNestedPath("");
@@ -1195,7 +1204,6 @@ void validatorWithErrors() {
11951204

11961205
errors.setNestedPath("spouse.");
11971206
assertThat(errors.getNestedPath()).isEqualTo("spouse.");
1198-
Validator spouseValidator = new SpouseValidator();
11991207
spouseValidator.validate(tb.getSpouse(), errors);
12001208

12011209
errors.setNestedPath("");
@@ -1267,7 +1275,6 @@ void validatorWithErrorsAndCodesPrefix() {
12671275

12681276
errors.setNestedPath("spouse.");
12691277
assertThat(errors.getNestedPath()).isEqualTo("spouse.");
1270-
Validator spouseValidator = new SpouseValidator();
12711278
spouseValidator.validate(tb.getSpouse(), errors);
12721279

12731280
errors.setNestedPath("");
@@ -1332,7 +1339,6 @@ void validatorWithNestedObjectNull() {
13321339
testValidator.validate(tb, errors);
13331340
errors.setNestedPath("spouse.");
13341341
assertThat(errors.getNestedPath()).isEqualTo("spouse.");
1335-
Validator spouseValidator = new SpouseValidator();
13361342
spouseValidator.validate(tb.getSpouse(), errors);
13371343
errors.setNestedPath("");
13381344

@@ -1348,7 +1354,6 @@ void nestedValidatorWithoutNestedPath() {
13481354
TestBean tb = new TestBean();
13491355
tb.setName("XXX");
13501356
Errors errors = new BeanPropertyBindingResult(tb, "tb");
1351-
Validator spouseValidator = new SpouseValidator();
13521357
spouseValidator.validate(tb, errors);
13531358

13541359
assertThat(errors.hasGlobalErrors()).isTrue();
@@ -2160,28 +2165,6 @@ public void validate(@Nullable Object obj, Errors errors) {
21602165
}
21612166
}
21622167

2163-
2164-
private static class SpouseValidator implements Validator {
2165-
2166-
@Override
2167-
public boolean supports(Class<?> clazz) {
2168-
return TestBean.class.isAssignableFrom(clazz);
2169-
}
2170-
2171-
@Override
2172-
public void validate(@Nullable Object obj, Errors errors) {
2173-
TestBean tb = (TestBean) obj;
2174-
if (tb == null || "XXX".equals(tb.getName())) {
2175-
errors.rejectValue("", "SPOUSE_NOT_AVAILABLE");
2176-
return;
2177-
}
2178-
if (tb.getAge() < 32) {
2179-
errors.rejectValue("age", "TOO_YOUNG", "simply too young");
2180-
}
2181-
}
2182-
}
2183-
2184-
21852168
@SuppressWarnings("unused")
21862169
private static class GrowingList<E> extends AbstractList<E> {
21872170

0 commit comments

Comments
 (0)