Merge pull request #29890 from making:support-biconsumer-validator
* gh-29890: Polish contribution Introduce functional factory methods in Validator
This commit is contained in:
commit
dd9f03d9b9
|
@ -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 <T> the target object type
|
||||
*/
|
||||
final class TypedValidator<T> implements Validator {
|
||||
|
||||
private final Class<T> targetClass;
|
||||
|
||||
private final Predicate<Class<?>> supports;
|
||||
|
||||
private final BiConsumer<T, Errors> validate;
|
||||
|
||||
|
||||
public TypedValidator(Class<T> targetClass, Predicate<Class<?>> supports, BiConsumer<T, Errors> 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>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
|
||||
* <p>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.
|
||||
*
|
||||
* <pre class="code">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.");
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
* <pre class="code">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.");
|
||||
* }
|
||||
* });</pre>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>For instance:
|
||||
* <pre class="code">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.");
|
||||
* }
|
||||
* });</pre>
|
||||
* @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 <T> the target object type
|
||||
* @return the created {@code Validator}
|
||||
* @since 6.1
|
||||
*/
|
||||
static <T> Validator forInstanceOf(Class<T> targetClass, BiConsumer<T, Errors> 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.
|
||||
*
|
||||
* <p>For instance:
|
||||
* <pre class="code">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.");
|
||||
* }
|
||||
* });</pre>
|
||||
* @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 <T> the target object type
|
||||
* @return the created {@code Validator}
|
||||
* @since 6.1
|
||||
*/
|
||||
static <T> Validator forType(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
|
||||
return new TypedValidator<>(targetClass, targetClass::equals, delegate);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<E> extends AbstractList<E> {
|
||||
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue