Skip to content

Commit

Permalink
Polish contribution
Browse files Browse the repository at this point in the history
- Split Validator::of into two factory methods:
  - forInstanceOf using Class::isAssignableFrom
  - forType using Class::equals
- Moved anonymous implementation into TypedValidator with default access

Closes gh-30341
  • Loading branch information
poutsma committed Apr 19, 2023
1 parent 5f98afc commit 3c57d55
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@

import java.util.function.BiConsumer;

import org.springframework.util.Assert;

/**
* A validator for application-specific objects.
*
Expand All @@ -30,40 +28,33 @@
* 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
* &amp;&amp; login.getPassword().trim().length() &lt; 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
* &amp;&amp; login.getPassword().trim().length() &lt; 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
Expand Down Expand Up @@ -97,38 +88,54 @@ public interface Validator {
*/
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:
* 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.
*
* <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
* <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 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);
}
static <T> Validator forInstanceOf(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
return new TypedValidator<>(targetClass, targetClass::isAssignableFrom, delegate);
}

@Override
public void validate(Object target, Errors errors) {
delegate.accept(targetClass.cast(target), errors);
}
};
/**
* 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
*/
class DataBinderTests {

Validator spouseValidator = Validator.of(TestBean.class, (tb, errors) -> {
private final Validator spouseValidator = Validator.forInstanceOf(TestBean.class, (tb, errors) -> {
if (tb == null || "XXX".equals(tb.getName())) {
errors.rejectValue("", "SPOUSE_NOT_AVAILABLE");
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
*/
public class ValidationUtilsTests {

Validator emptyValidator = Validator.of(TestBean.class, (testBean, errors) -> ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!"));
private final Validator emptyValidator = Validator.forInstanceOf(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!"));
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}

}

1 comment on commit 3c57d55

@poutsma
Copy link
Contributor Author

@poutsma poutsma commented on 3c57d55 Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit had an incorrect commit message, it should have closed #29890; not #30341.

Please sign in to comment.