From 10cb2322e9d0ba40f612e1a643ece96d2dcbfc79 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Jul 2023 21:56:44 +0200 Subject: [PATCH] Introduce Validator.validateObject(Object) with returned Errors Includes failOnError method on Errors interface for use with validateObject. Declares many Errors methods as default methods for lean SimpleErrors class. Closes gh-19877 --- .../validation/AbstractErrors.java | 117 +------ .../springframework/validation/Errors.java | 183 ++++++++--- .../validation/SimpleErrors.java | 181 +++++++++++ .../springframework/validation/Validator.java | 41 ++- .../validation/DataBinderTests.java | 298 +++++++++++------- .../validation/ValidationUtilsTests.java | 70 ++-- 6 files changed, 585 insertions(+), 305 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/validation/SimpleErrors.java diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java index 9556dc3a286..3886e085942 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -28,13 +28,14 @@ import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** - * Abstract implementation of the {@link Errors} interface. Provides common - * access to evaluated errors; however, does not define concrete management + * Abstract implementation of the {@link Errors} interface. + * Provides nested path handling but does not define concrete management * of {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}. * * @author Juergen Hoeller * @author Rossen Stoyanchev * @since 2.5.3 + * @see AbstractBindingResult */ @SuppressWarnings("serial") public abstract class AbstractErrors implements Errors, Serializable { @@ -81,8 +82,8 @@ public abstract class AbstractErrors implements Errors, Serializable { nestedPath = ""; } nestedPath = canonicalFieldName(nestedPath); - if (nestedPath.length() > 0 && !nestedPath.endsWith(Errors.NESTED_PATH_SEPARATOR)) { - nestedPath += Errors.NESTED_PATH_SEPARATOR; + if (nestedPath.length() > 0 && !nestedPath.endsWith(NESTED_PATH_SEPARATOR)) { + nestedPath += NESTED_PATH_SEPARATOR; } this.nestedPath = nestedPath; } @@ -97,7 +98,7 @@ public abstract class AbstractErrors implements Errors, Serializable { } else { String path = getNestedPath(); - return (path.endsWith(Errors.NESTED_PATH_SEPARATOR) ? + return (path.endsWith(NESTED_PATH_SEPARATOR) ? path.substring(0, path.length() - NESTED_PATH_SEPARATOR.length()) : path); } } @@ -112,117 +113,19 @@ public abstract class AbstractErrors implements Errors, Serializable { return field; } - - @Override - public void reject(String errorCode) { - reject(errorCode, null, null); - } - - @Override - public void reject(String errorCode, String defaultMessage) { - reject(errorCode, null, defaultMessage); - } - - @Override - public void rejectValue(@Nullable String field, String errorCode) { - rejectValue(field, errorCode, null, null); - } - - @Override - public void rejectValue(@Nullable String field, String errorCode, String defaultMessage) { - rejectValue(field, errorCode, null, defaultMessage); - } - - - @Override - public boolean hasErrors() { - return !getAllErrors().isEmpty(); - } - - @Override - public int getErrorCount() { - return getAllErrors().size(); - } - - @Override - public List getAllErrors() { - List result = new ArrayList<>(); - result.addAll(getGlobalErrors()); - result.addAll(getFieldErrors()); - return Collections.unmodifiableList(result); - } - - @Override - public boolean hasGlobalErrors() { - return (getGlobalErrorCount() > 0); - } - - @Override - public int getGlobalErrorCount() { - return getGlobalErrors().size(); - } - - @Override - @Nullable - public ObjectError getGlobalError() { - List globalErrors = getGlobalErrors(); - return (!globalErrors.isEmpty() ? globalErrors.get(0) : null); - } - - @Override - public boolean hasFieldErrors() { - return (getFieldErrorCount() > 0); - } - - @Override - public int getFieldErrorCount() { - return getFieldErrors().size(); - } - - @Override - @Nullable - public FieldError getFieldError() { - List fieldErrors = getFieldErrors(); - return (!fieldErrors.isEmpty() ? fieldErrors.get(0) : null); - } - - @Override - public boolean hasFieldErrors(String field) { - return (getFieldErrorCount(field) > 0); - } - - @Override - public int getFieldErrorCount(String field) { - return getFieldErrors(field).size(); - } - @Override public List getFieldErrors(String field) { List fieldErrors = getFieldErrors(); List result = new ArrayList<>(); String fixedField = fixedField(field); - for (FieldError error : fieldErrors) { - if (isMatchingFieldError(fixedField, error)) { - result.add(error); + for (FieldError fieldError : fieldErrors) { + if (isMatchingFieldError(fixedField, fieldError)) { + result.add(fieldError); } } return Collections.unmodifiableList(result); } - @Override - @Nullable - public FieldError getFieldError(String field) { - List fieldErrors = getFieldErrors(field); - return (!fieldErrors.isEmpty() ? fieldErrors.get(0) : null); - } - - @Override - @Nullable - public Class getFieldType(String field) { - Object value = getFieldValue(field); - return (value != null ? value.getClass() : null); - } - /** * Check whether the given FieldError matches the given field. * @param field the field that we are looking up FieldErrors for diff --git a/spring-context/src/main/java/org/springframework/validation/Errors.java b/spring-context/src/main/java/org/springframework/validation/Errors.java index 45ae5d7b57a..221b06d9c7b 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -17,29 +17,33 @@ package org.springframework.validation; import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; import org.springframework.beans.PropertyAccessor; import org.springframework.lang.Nullable; /** - * Stores and exposes information about data-binding and validation - * errors for a specific object. + * Stores and exposes information about data-binding and validation errors + * for a specific object. * - *

Field names can be properties of the target object (e.g. "name" - * when binding to a customer object), or nested fields in case of - * subobjects (e.g. "address.street"). Supports subtree navigation - * via {@link #setNestedPath(String)}: for example, an - * {@code AddressValidator} validates "address", not being aware - * that this is a subobject of customer. + *

Field names are typically properties of the target object (e.g. "name" + * when binding to a customer object). Implementations may also support nested + * fields in case of nested objects (e.g. "address.street"), in conjunction + * with subtree navigation via {@link #setNestedPath}: for example, an + * {@code AddressValidator} may validate "address", not being aware that this + * is a nested object of a top-level customer object. * *

Note: {@code Errors} objects are single-threaded. * * @author Rod Johnson * @author Juergen Hoeller * @see #setNestedPath - * @see BindException - * @see DataBinder + * @see Validator * @see ValidationUtils + * @see SimpleErrors + * @see BindingResult */ public interface Errors { @@ -63,18 +67,26 @@ public interface Errors { * subtrees. Reject calls prepend the given path to the field names. *

For example, an address validator could validate the subobject * "address" of a customer object. + *

The default implementation throws {@code UnsupportedOperationException} + * since not all {@code Errors} implementations support nested paths. * @param nestedPath nested path within this object, * e.g. "address" (defaults to "", {@code null} is also acceptable). * Can end with a dot: both "address" and "address." are valid. + * @see #getNestedPath() */ - void setNestedPath(String nestedPath); + default void setNestedPath(String nestedPath) { + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not support nested paths"); + } /** * Return the current nested path of this {@link Errors} object. *

Returns a nested path with a dot, i.e. "address.", for easy * building of concatenated paths. Default is an empty String. + * @see #setNestedPath(String) */ - String getNestedPath(); + default String getNestedPath() { + return ""; + } /** * Push the given sub path onto the nested path stack. @@ -85,32 +97,44 @@ public interface Errors { * for subobjects without having to worry about a temporary path holder. *

For example: current path "spouse.", pushNestedPath("child") → * result path "spouse.child."; popNestedPath() → "spouse." again. + *

The default implementation throws {@code UnsupportedOperationException} + * since not all {@code Errors} implementations support nested paths. * @param subPath the sub path to push onto the nested path stack - * @see #popNestedPath + * @see #popNestedPath() */ - void pushNestedPath(String subPath); + default void pushNestedPath(String subPath) { + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not support nested paths"); + } /** * Pop the former nested path from the nested path stack. * @throws IllegalStateException if there is no former nested path on the stack - * @see #pushNestedPath + * @see #pushNestedPath(String) */ - void popNestedPath() throws IllegalStateException; + default void popNestedPath() throws IllegalStateException { + throw new IllegalStateException("Cannot pop nested path: no nested path on stack"); + } /** * Register a global error for the entire target object, * using the given error description. * @param errorCode error code, interpretable as a message key + * @see #reject(String, Object[], String) */ - void reject(String errorCode); + default void reject(String errorCode) { + reject(errorCode, null, null); + } /** * Register a global error for the entire target object, * using the given error description. * @param errorCode error code, interpretable as a message key * @param defaultMessage fallback default message + * @see #reject(String, Object[], String) */ - void reject(String errorCode, String defaultMessage); + default void reject(String errorCode, String defaultMessage) { + reject(errorCode, null, defaultMessage); + } /** * Register a global error for the entire target object, @@ -119,6 +143,7 @@ public interface Errors { * @param errorArgs error arguments, for argument binding via MessageFormat * (can be {@code null}) * @param defaultMessage fallback default message + * @see #rejectValue(String, String, Object[], String) */ void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); @@ -132,9 +157,11 @@ public interface Errors { * global error if the current object is the top object. * @param field the field name (may be {@code null} or empty String) * @param errorCode error code, interpretable as a message key - * @see #getNestedPath() + * @see #rejectValue(String, String, Object[], String) */ - void rejectValue(@Nullable String field, String errorCode); + default void rejectValue(@Nullable String field, String errorCode) { + rejectValue(field, errorCode, null, null); + } /** * Register a field error for the specified field of the current object @@ -147,9 +174,11 @@ public interface Errors { * @param field the field name (may be {@code null} or empty String) * @param errorCode error code, interpretable as a message key * @param defaultMessage fallback default message - * @see #getNestedPath() + * @see #rejectValue(String, String, Object[], String) */ - void rejectValue(@Nullable String field, String errorCode, String defaultMessage); + default void rejectValue(@Nullable String field, String errorCode, String defaultMessage) { + rejectValue(field, errorCode, null, defaultMessage); + } /** * Register a field error for the specified field of the current object @@ -164,7 +193,7 @@ public interface Errors { * @param errorArgs error arguments, for argument binding via MessageFormat * (can be {@code null}) * @param defaultMessage fallback default message - * @see #getNestedPath() + * @see #reject(String, Object[], String) */ void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); @@ -178,110 +207,169 @@ public interface Errors { *

Note that the passed-in {@code Errors} instance is supposed * to refer to the same target object, or at least contain compatible errors * that apply to the target object of this {@code Errors} instance. + *

The default implementation throws {@code UnsupportedOperationException} + * since not all {@code Errors} implementations support {@code #addAllErrors}. * @param errors the {@code Errors} instance to merge in + * @see #getAllErrors() */ - void addAllErrors(Errors errors); + default void addAllErrors(Errors errors) { + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not support addAllErrors"); + } /** - * Return if there were any errors. + * Throw the mapped exception with a message summarizing the recorded errors. + * @param messageToException a function mapping the message to the exception, + * e.g. {@code IllegalArgumentException::new} or {@code IllegalStateException::new} + * @param the exception type to be thrown + * @since 6.1 + * @see #toString() + */ + default void failOnError(Function messageToException) throws T { + if (hasErrors()) { + throw messageToException.apply(toString()); + } + } + + /** + * BReturn if there were any errors. + * @see #hasGlobalErrors() + * @see #hasFieldErrors() */ - boolean hasErrors(); + default boolean hasErrors() { + return (!getGlobalErrors().isEmpty() || !getFieldErrors().isEmpty()); + } /** * Return the total number of errors. + * @see #getGlobalErrorCount() + * @see #getFieldErrorCount() */ - int getErrorCount(); + default int getErrorCount() { + return (getGlobalErrors().size() + getFieldErrors().size()); + } /** * Get all errors, both global and field ones. * @return a list of {@link ObjectError} instances + * @see #getGlobalErrors() + * @see #getFieldErrors() */ - List getAllErrors(); + default List getAllErrors() { + return Stream.concat(getGlobalErrors().stream(), getFieldErrors().stream()).toList(); + } /** * Are there any global errors? * @return {@code true} if there are any global errors * @see #hasFieldErrors() */ - boolean hasGlobalErrors(); + default boolean hasGlobalErrors() { + return !getGlobalErrors().isEmpty(); + } /** * Return the number of global errors. * @return the number of global errors * @see #getFieldErrorCount() */ - int getGlobalErrorCount(); + default int getGlobalErrorCount() { + return getGlobalErrors().size(); + } /** * Get all global errors. * @return a list of {@link ObjectError} instances + * @see #getFieldErrors() */ List getGlobalErrors(); /** * Get the first global error, if any. * @return the global error, or {@code null} + * @see #getFieldError() */ @Nullable - ObjectError getGlobalError(); + default ObjectError getGlobalError() { + return getGlobalErrors().stream().findFirst().orElse(null); + } /** * Are there any field errors? * @return {@code true} if there are any errors associated with a field * @see #hasGlobalErrors() */ - boolean hasFieldErrors(); + default boolean hasFieldErrors() { + return !getFieldErrors().isEmpty(); + } /** * Return the number of errors associated with a field. * @return the number of errors associated with a field * @see #getGlobalErrorCount() */ - int getFieldErrorCount(); + default int getFieldErrorCount() { + return getFieldErrors().size(); + } /** * Get all errors associated with a field. * @return a List of {@link FieldError} instances + * @see #getGlobalErrors() */ List getFieldErrors(); /** * Get the first error associated with a field, if any. * @return the field-specific error, or {@code null} + * @see #getGlobalError() */ @Nullable - FieldError getFieldError(); + default FieldError getFieldError() { + return getFieldErrors().stream().findFirst().orElse(null); + } /** * Are there any errors associated with the given field? * @param field the field name * @return {@code true} if there were any errors associated with the given field + * @see #hasFieldErrors() */ - boolean hasFieldErrors(String field); + default boolean hasFieldErrors(String field) { + return (getFieldError(field) != null); + } /** * Return the number of errors associated with the given field. * @param field the field name * @return the number of errors associated with the given field + * @see #getFieldErrorCount() */ - int getFieldErrorCount(String field); + default int getFieldErrorCount(String field) { + return getFieldErrors(field).size(); + } /** * Get all errors associated with the given field. - *

Implementations should support not only full field names like - * "name" but also pattern matches like "na*" or "address.*". + *

Implementations may support not only full field names like + * "address.street" but also pattern matches like "address.*". * @param field the field name * @return a List of {@link FieldError} instances + * @see #getFieldErrors() */ - List getFieldErrors(String field); + default List getFieldErrors(String field) { + return getFieldErrors().stream().filter(error -> field.equals(error.getField())).toList(); + } /** * Get the first error associated with the given field, if any. * @param field the field name * @return the field-specific error, or {@code null} + * @see #getFieldError() */ @Nullable - FieldError getFieldError(String field); + default FieldError getFieldError(String field) { + return getFieldErrors().stream().filter(error -> field.equals(error.getField())).findFirst().orElse(null); + } /** * Return the current value of the given field, either the current @@ -290,6 +378,7 @@ public interface Errors { * even if there were type mismatches. * @param field the field name * @return the current value of the given field + * @see #getFieldType(String) */ @Nullable Object getFieldValue(String field); @@ -301,8 +390,18 @@ public interface Errors { * associated descriptor. * @param field the field name * @return the type of the field, or {@code null} if not determinable + * @see #getFieldValue(String) */ @Nullable - Class getFieldType(String field); + default Class getFieldType(String field) { + return Optional.ofNullable(getFieldValue(field)).map(Object::getClass).orElse(null); + } + + /** + * Return a summary of the recorded errors, + * e.g. for inclusion in an exception message. + * @see #failOnError(Function) + */ + String toString(); } diff --git a/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java b/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java new file mode 100644 index 00000000000..8e42f76bbc9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java @@ -0,0 +1,181 @@ +/* + * 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.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.BeanUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A simple implementation of the {@link Errors} interface, managing global + * errors and field errors for a top-level target object. Flexibly retrieves + * field values through bean property getter methods, and automatically + * falls back to raw field access if necessary. + * + *

Note that this {@link Errors} implementation comes without support for + * nested paths. It is exclusively designed for the validation of individual + * top-level objects, not aggregating errors from multiple sources. + * If this is insufficient for your purposes, use a binding-capable + * {@link Errors} implementation such as {@link BeanPropertyBindingResult}. + * + * @author Juergen Hoeller + * @since 6.1 + * @see Validator#validateObject(Object) + * @see BeanPropertyBindingResult + * @see DirectFieldBindingResult + */ +@SuppressWarnings("serial") +public class SimpleErrors implements Errors, Serializable { + + private final Object target; + + private final String objectName; + + private final List globalErrors = new ArrayList<>(); + + private final List fieldErrors = new ArrayList<>(); + + + /** + * Create a new {@link SimpleErrors} holder for the given target, + * using the simple name of the target class as the object name. + * @param target the target to wrap + */ + public SimpleErrors(Object target) { + Assert.notNull(target, "Target must not be null"); + this.target = target; + this.objectName = this.target.getClass().getSimpleName(); + } + + /** + * Create a new {@link SimpleErrors} holder for the given target. + * @param target the target to wrap + * @param objectName the name of the target object for error reporting + */ + public SimpleErrors(Object target, String objectName) { + Assert.notNull(target, "Target must not be null"); + this.target = target; + this.objectName = objectName; + } + + + @Override + public String getObjectName() { + return this.objectName; + } + + @Override + public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + this.globalErrors.add(new ObjectError(getObjectName(), new String[] {errorCode}, errorArgs, defaultMessage)); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + + if (!StringUtils.hasLength(field)) { + reject(errorCode, errorArgs, defaultMessage); + return; + } + + Object newVal = getFieldValue(field); + this.fieldErrors.add(new FieldError(getObjectName(), field, newVal, false, + new String[] {errorCode}, errorArgs, defaultMessage)); + } + + @Override + public void addAllErrors(Errors errors) { + this.globalErrors.addAll(errors.getGlobalErrors()); + this.fieldErrors.addAll(errors.getFieldErrors()); + } + + @Override + public List getGlobalErrors() { + return this.globalErrors; + } + + @Override + public List getFieldErrors() { + return this.fieldErrors; + } + + @Override + @Nullable + public Object getFieldValue(String field) { + PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(this.target.getClass(), field); + if (pd != null && pd.getReadMethod() != null) { + return ReflectionUtils.invokeMethod(pd.getReadMethod(), this.target); + } + Field rawField = ReflectionUtils.findField(this.target.getClass(), field); + if (rawField != null) { + ReflectionUtils.makeAccessible(rawField); + return ReflectionUtils.getField(rawField, this.target); + } + throw new IllegalArgumentException("Cannot retrieve value for field '" + field + + "' - neither a getter method nor a raw field found"); + } + + @Override + public Class getFieldType(String field) { + PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(this.target.getClass(), field); + if (pd != null) { + return pd.getPropertyType(); + } + Field rawField = ReflectionUtils.findField(this.target.getClass(), field); + if (rawField != null) { + return rawField.getType(); + } + return null; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof SimpleErrors that && + ObjectUtils.nullSafeEquals(this.target, that.target) && + this.globalErrors.equals(that.globalErrors) && + this.fieldErrors.equals(that.fieldErrors))); + } + + @Override + public int hashCode() { + return this.target.hashCode(); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (ObjectError error : this.globalErrors) { + sb.append('\n').append(error); + } + for (ObjectError error : this.fieldErrors) { + sb.append('\n').append(error); + } + return sb.toString(); + } + +} 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 f4ab6c672b5..bea33261aeb 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -48,16 +48,17 @@ import java.util.function.BiConsumer; * } * }); * - *

See also the Spring reference manual for a fuller discussion of - * the {@code Validator} interface and its role in an enterprise - * application. + *

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 Juergen Hoeller * @author Toshiaki Maki * @author Arjen Poutsma * @see SmartValidator * @see Errors * @see ValidationUtils + * @see DataBinder#setValidator */ public interface Validator { @@ -77,17 +78,43 @@ public interface Validator { boolean supports(Class clazz); /** - * Validate the supplied {@code target} object, which must be - * of a {@link Class} for which the {@link #supports(Class)} method - * typically has (or would) return {@code true}. + * Validate the given {@code target} object which must be of a + * {@link Class} for which the {@link #supports(Class)} method + * typically has returned (or would return) {@code true}. *

The supplied {@link Errors errors} instance can be used to report - * any resulting validation errors. + * any resulting validation errors, typically as part of a larger + * binding process which this validator is meant to participate in. + * Binding errors have typically been pre-registered with the + * {@link Errors errors} instance before this invocation already. * @param target the object that is to be validated * @param errors contextual state about the validation process * @see ValidationUtils */ void validate(Object target, Errors errors); + /** + * Validate the given {@code target} object individually. + *

Delegates to the common {@link #validate(Object, Errors)} method. + * The returned {@link Errors errors} instance can be used to report + * any resulting validation errors for the specific target object, e.g. + * {@code if (validator.validateObject(target).hasErrors()) ...} or + * {@code validator.validateObject(target).failOnError(IllegalStateException::new));}. + *

Note: This validation call comes with limitations in the {@link Errors} + * implementation used, in particular no support for nested paths. + * If this is insufficient for your purposes, call the regular + * {@link #validate(Object, Errors)} method with a binding-capable + * {@link Errors} implementation such as {@link BeanPropertyBindingResult}. + * @param target the object that is to be validated + * @return resulting errors from the validation of the given object + * @since 6.1 + * @see SimpleErrors + */ + default Errors validateObject(Object target) { + Errors errors = new SimpleErrors(target); + validate(target, errors); + return errors; + } + /** * Return a {@code Validator} that checks whether the target object 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 e4c4ae97c1e..a5cc5ba0a98 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -79,6 +79,7 @@ import static org.assertj.core.api.Assertions.entry; * @author Rob Harrop * @author Kazuki Shimizu * @author Sam Brannen + * @author Arjen Poutsma */ class DataBinderTests { @@ -92,6 +93,7 @@ class DataBinderTests { } }); + @Test void bindingNoErrors() throws BindException { TestBean rod = new TestBean(); @@ -113,7 +115,7 @@ class DataBinderTests { TestBean tb = (TestBean) map.get("person"); assertThat(tb.equals(rod)).as("Same object").isTrue(); - BindingResult other = new BeanPropertyBindingResult(rod, "person"); + BindingResult other = new DataBinder(rod, "person").getBindingResult(); assertThat(binder.getBindingResult()).isEqualTo(other); assertThat(other).isEqualTo(binder.getBindingResult()); BindException ex = new BindException(other); @@ -167,8 +169,9 @@ class DataBinderTests { pvs.add("name", "Rod"); pvs.add("age", 32); pvs.add("nonExisting", "someValue"); - assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> - binder.bind(pvs)); + + assertThatExceptionOfType(NotWritablePropertyException.class) + .isThrownBy(() -> binder.bind(pvs)); } @Test @@ -178,8 +181,9 @@ class DataBinderTests { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("spouse.age", 32); - assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> - binder.bind(pvs)); + + assertThatExceptionOfType(NullValueInNestedPathException.class) + .isThrownBy(() -> binder.bind(pvs)); } @Test @@ -207,57 +211,56 @@ class DataBinderTests { pvs.add("age", "32x"); pvs.add("touchy", "m.y"); binder.bind(pvs); - assertThatExceptionOfType(BindException.class).isThrownBy( - binder::close) - .satisfies(ex -> { - assertThat(rod.getName()).isEqualTo("Rod"); - Map map = binder.getBindingResult().getModel(); - TestBean tb = (TestBean) map.get("person"); - assertThat(tb).isSameAs(rod); - - BindingResult br = (BindingResult) map.get(BindingResult.MODEL_KEY_PREFIX + "person"); - assertThat(BindingResultUtils.getBindingResult(map, "person")).isEqualTo(br); - assertThat(BindingResultUtils.getRequiredBindingResult(map, "person")).isEqualTo(br); - - assertThat(BindingResultUtils.getBindingResult(map, "someOtherName")).isNull(); - assertThatIllegalStateException().isThrownBy(() -> - BindingResultUtils.getRequiredBindingResult(map, "someOtherName")); - - assertThat(binder.getBindingResult()).as("Added itself to map").isSameAs(br); - assertThat(br.hasErrors()).isTrue(); - assertThat(br.getErrorCount()).isEqualTo(2); - - assertThat(br.hasFieldErrors("age")).isTrue(); - assertThat(br.getFieldErrorCount("age")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); - FieldError ageError = binder.getBindingResult().getFieldError("age"); - assertThat(ageError).isNotNull(); - assertThat(ageError.getCode()).isEqualTo("typeMismatch"); - assertThat(ageError.getRejectedValue()).isEqualTo("32x"); - assertThat(ageError.contains(TypeMismatchException.class)).isTrue(); - assertThat(ageError.contains(NumberFormatException.class)).isTrue(); - assertThat(ageError.unwrap(NumberFormatException.class).getMessage()).contains("32x"); - assertThat(tb.getAge()).isEqualTo(0); - - assertThat(br.hasFieldErrors("touchy")).isTrue(); - assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); - FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); - assertThat(touchyError).isNotNull(); - assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); - assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); - assertThat(touchyError.contains(MethodInvocationException.class)).isTrue(); - assertThat(touchyError.unwrap(MethodInvocationException.class).getCause().getMessage()).contains("a ."); - assertThat(tb.getTouchy()).isNull(); - - DataBinder binder2 = new DataBinder(new TestBean(), "person"); - MutablePropertyValues pvs2 = new MutablePropertyValues(); - pvs2.add("name", "Rod"); - pvs2.add("age", "32x"); - pvs2.add("touchy", "m.y"); - binder2.bind(pvs2); - assertThat(ex.getBindingResult()).isEqualTo(binder2.getBindingResult()); - }); + + assertThatExceptionOfType(BindException.class).isThrownBy(binder::close).satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map map = binder.getBindingResult().getModel(); + TestBean tb = (TestBean) map.get("person"); + assertThat(tb).isSameAs(rod); + + BindingResult br = (BindingResult) map.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(BindingResultUtils.getBindingResult(map, "person")).isEqualTo(br); + assertThat(BindingResultUtils.getRequiredBindingResult(map, "person")).isEqualTo(br); + + assertThat(BindingResultUtils.getBindingResult(map, "someOtherName")).isNull(); + assertThatIllegalStateException().isThrownBy(() -> + BindingResultUtils.getRequiredBindingResult(map, "someOtherName")); + + assertThat(binder.getBindingResult()).as("Added itself to map").isSameAs(br); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(2); + + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + FieldError ageError = binder.getBindingResult().getFieldError("age"); + assertThat(ageError).isNotNull(); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + assertThat(ageError.getRejectedValue()).isEqualTo("32x"); + assertThat(ageError.contains(TypeMismatchException.class)).isTrue(); + assertThat(ageError.contains(NumberFormatException.class)).isTrue(); + assertThat(ageError.unwrap(NumberFormatException.class).getMessage()).contains("32x"); + assertThat(tb.getAge()).isEqualTo(0); + + assertThat(br.hasFieldErrors("touchy")).isTrue(); + assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); + FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); + assertThat(touchyError).isNotNull(); + assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); + assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); + assertThat(touchyError.contains(MethodInvocationException.class)).isTrue(); + assertThat(touchyError.unwrap(MethodInvocationException.class).getCause().getMessage()).contains("a ."); + assertThat(tb.getTouchy()).isNull(); + + DataBinder binder2 = new DataBinder(new TestBean(), "person"); + MutablePropertyValues pvs2 = new MutablePropertyValues(); + pvs2.add("name", "Rod"); + pvs2.add("age", "32x"); + pvs2.add("touchy", "m.y"); + binder2.bind(pvs2); + assertThat(ex.getBindingResult()).isEqualTo(binder2.getBindingResult()); + }); } @Test @@ -267,15 +270,17 @@ class DataBinderTests { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("class.classLoader.URLs[0]", "https://myserver"); binder.setIgnoreUnknownFields(false); - assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> - binder.bind(pvs)) - .withMessageContaining("classLoader"); + + assertThatExceptionOfType(NotWritablePropertyException.class) + .isThrownBy(() -> binder.bind(pvs)) + .withMessageContaining("classLoader"); } @Test void bindingWithErrorsAndCustomEditors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); + binder.registerCustomEditor(String.class, "touchy", new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { @@ -296,6 +301,7 @@ class DataBinderTests { return ((TestBean) getValue()).getName(); } }); + MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("age", "32x"); @@ -303,41 +309,39 @@ class DataBinderTests { pvs.add("spouse", "Kerry"); binder.bind(pvs); - assertThatExceptionOfType(BindException.class).isThrownBy( - binder::close) - .satisfies(ex -> { - assertThat(rod.getName()).isEqualTo("Rod"); - Map model = binder.getBindingResult().getModel(); - TestBean tb = (TestBean) model.get("person"); - assertThat(tb).isEqualTo(rod); - - BindingResult br = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "person"); - assertThat(binder.getBindingResult()).isSameAs(br); - assertThat(br.hasErrors()).isTrue(); - assertThat(br.getErrorCount()).isEqualTo(2); - - assertThat(br.hasFieldErrors("age")).isTrue(); - assertThat(br.getFieldErrorCount("age")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); - FieldError ageError = binder.getBindingResult().getFieldError("age"); - assertThat(ageError).isNotNull(); - assertThat(ageError.getCode()).isEqualTo("typeMismatch"); - assertThat(ageError.getRejectedValue()).isEqualTo("32x"); - assertThat(tb.getAge()).isEqualTo(0); - - assertThat(br.hasFieldErrors("touchy")).isTrue(); - assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); - assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); - FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); - assertThat(touchyError).isNotNull(); - assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); - assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); - assertThat(tb.getTouchy()).isNull(); - - assertThat(br.hasFieldErrors("spouse")).isFalse(); - assertThat(binder.getBindingResult().getFieldValue("spouse")).isEqualTo("Kerry"); - assertThat(tb.getSpouse()).isNotNull(); - }); + assertThatExceptionOfType(BindException.class).isThrownBy(binder::close).satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map model = binder.getBindingResult().getModel(); + TestBean tb = (TestBean) model.get("person"); + assertThat(tb).isEqualTo(rod); + + BindingResult br = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(binder.getBindingResult()).isSameAs(br); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(2); + + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + FieldError ageError = binder.getBindingResult().getFieldError("age"); + assertThat(ageError).isNotNull(); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + assertThat(ageError.getRejectedValue()).isEqualTo("32x"); + assertThat(tb.getAge()).isEqualTo(0); + + assertThat(br.hasFieldErrors("touchy")).isTrue(); + assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); + FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); + assertThat(touchyError).isNotNull(); + assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); + assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); + assertThat(tb.getTouchy()).isNull(); + + assertThat(br.hasFieldErrors("spouse")).isFalse(); + assertThat(binder.getBindingResult().getFieldValue("spouse")).isEqualTo("Kerry"); + assertThat(tb.getSpouse()).isNotNull(); + }); } @Test @@ -1144,12 +1148,11 @@ class DataBinderTests { tb2.setAge(34); tb.setSpouse(tb2); DataBinder db = new DataBinder(tb, "tb"); + db.setValidator(new TestBeanValidator()); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("spouse.age", "argh"); db.bind(pvs); Errors errors = db.getBindingResult(); - Validator testValidator = new TestBeanValidator(); - testValidator.validate(tb, errors); errors.setNestedPath("spouse"); assertThat(errors.getNestedPath()).isEqualTo("spouse."); @@ -1196,8 +1199,7 @@ class DataBinderTests { void validatorWithErrors() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); - - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new DataBinder(tb, "tb").getBindingResult(); Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); @@ -1209,7 +1211,11 @@ class DataBinderTests { errors.setNestedPath(""); assertThat(errors.hasErrors()).isTrue(); assertThat(errors.getErrorCount()).isEqualTo(6); + assertThat(errors.getAllErrors()) + .containsAll(errors.getGlobalErrors()) + .containsAll(errors.getFieldErrors()); + assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.getGlobalErrorCount()).isEqualTo(2); assertThat(errors.getGlobalError().getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); @@ -1265,10 +1271,11 @@ class DataBinderTests { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(tb, "tb"); + DataBinder dataBinder = new DataBinder(tb, "tb"); DefaultMessageCodesResolver codesResolver = new DefaultMessageCodesResolver(); codesResolver.setPrefix("validation."); - errors.setMessageCodesResolver(codesResolver); + dataBinder.setMessageCodesResolver(codesResolver); + Errors errors = dataBinder.getBindingResult(); Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); @@ -1280,7 +1287,11 @@ class DataBinderTests { errors.setNestedPath(""); assertThat(errors.hasErrors()).isTrue(); assertThat(errors.getErrorCount()).isEqualTo(6); + assertThat(errors.getAllErrors()) + .containsAll(errors.getGlobalErrors()) + .containsAll(errors.getFieldErrors()); + assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.getGlobalErrorCount()).isEqualTo(2); assertThat(errors.getGlobalError().getCode()).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); @@ -1331,12 +1342,63 @@ class DataBinderTests { assertThat((errors.getFieldErrors("spouse.age").get(0)).getRejectedValue()).isEqualTo(0); } + @Test + void validateObjectWithErrors() { + TestBean tb = new TestBean(); + Errors errors = new SimpleErrors(tb, "tb"); + + Validator testValidator = new TestBeanValidator(); + testValidator.validate(tb, errors); + + assertThat(errors.hasErrors()).isTrue(); + assertThat(errors.getErrorCount()).isEqualTo(5); + assertThat(errors.getAllErrors()) + .containsAll(errors.getGlobalErrors()) + .containsAll(errors.getFieldErrors()); + + assertThat(errors.hasGlobalErrors()).isTrue(); + assertThat(errors.getGlobalErrorCount()).isEqualTo(2); + assertThat(errors.getGlobalError().getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getGlobalErrors().get(1)).getCode()).isEqualTo("GENERAL_ERROR"); + assertThat((errors.getGlobalErrors().get(1)).getDefaultMessage()).isEqualTo("msg"); + assertThat((errors.getGlobalErrors().get(1)).getArguments()[0]).isEqualTo("arg"); + + assertThat(errors.hasFieldErrors()).isTrue(); + assertThat(errors.getFieldErrorCount()).isEqualTo(3); + assertThat(errors.getFieldError().getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(0)).getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(0)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors().get(1)).getCode()).isEqualTo("AGE_NOT_ODD"); + assertThat((errors.getFieldErrors().get(1)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors().get(2)).getCode()).isEqualTo("NOT_ROD"); + assertThat((errors.getFieldErrors().get(2)).getField()).isEqualTo("name"); + + assertThat(errors.hasFieldErrors("age")).isTrue(); + assertThat(errors.getFieldErrorCount("age")).isEqualTo(2); + assertThat(errors.getFieldError("age").getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors("age").get(0)).getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors("age").get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getFieldErrors("age").get(0)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors("age").get(0)).getRejectedValue()).isEqualTo(0); + assertThat((errors.getFieldErrors("age").get(1)).getCode()).isEqualTo("AGE_NOT_ODD"); + + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldErrorCount("name")).isEqualTo(1); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("NOT_ROD"); + assertThat((errors.getFieldErrors("name").get(0)).getField()).isEqualTo("name"); + assertThat((errors.getFieldErrors("name").get(0)).getRejectedValue()).isNull(); + } + @Test void validatorWithNestedObjectNull() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new DataBinder(tb, "tb").getBindingResult(); + Validator testValidator = new TestBeanValidator(); testValidator.validate(tb, errors); + errors.setNestedPath("spouse."); assertThat(errors.getNestedPath()).isEqualTo("spouse."); spouseValidator.validate(tb.getSpouse(), errors); @@ -1353,13 +1415,12 @@ class DataBinderTests { void nestedValidatorWithoutNestedPath() { TestBean tb = new TestBean(); tb.setName("XXX"); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - spouseValidator.validate(tb, errors); + Errors errors = spouseValidator.validateObject(tb); assertThat(errors.hasGlobalErrors()).isTrue(); assertThat(errors.getGlobalErrorCount()).isEqualTo(1); assertThat(errors.getGlobalError().getCode()).isEqualTo("SPOUSE_NOT_AVAILABLE"); - assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("TestBean"); } @Test @@ -1770,7 +1831,7 @@ class DataBinderTests { binder.bind(pvs); Errors errors = binder.getBindingResult(); - BeanPropertyBindingResult errors2 = new BeanPropertyBindingResult(rod, "person"); + Errors errors2 = new SimpleErrors(rod, "person"); errors.rejectValue("name", "badName"); errors.addAllErrors(errors2); @@ -1805,16 +1866,16 @@ class DataBinderTests { tb.setName("myName"); tb.setAge(99); - BeanPropertyBindingResult ex = new BeanPropertyBindingResult(tb, "tb"); - ex.reject("invalid"); - ex.rejectValue("age", "invalidField"); + Errors errors = new SimpleErrors(tb, "tb"); + errors.reject("invalid"); + errors.rejectValue("age", "invalidField"); StaticMessageSource ms = new StaticMessageSource(); ms.addMessage("invalid", Locale.US, "general error"); ms.addMessage("invalidField", Locale.US, "invalid field"); - assertThat(ms.getMessage(ex.getGlobalError(), Locale.US)).isEqualTo("general error"); - assertThat(ms.getMessage(ex.getFieldError("age"), Locale.US)).isEqualTo("invalid field"); + assertThat(ms.getMessage(errors.getGlobalError(), Locale.US)).isEqualTo("general error"); + assertThat(ms.getMessage(errors.getFieldError("age"), Locale.US)).isEqualTo("invalid field"); } @Test @@ -1882,13 +1943,13 @@ class DataBinderTests { void autoGrowBeyondDefaultLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); - MutablePropertyValues mpvs = new MutablePropertyValues(); mpvs.add("friends[256]", ""); + assertThatExceptionOfType(InvalidPropertyException.class) - .isThrownBy(() -> binder.bind(mpvs)) - .havingRootCause() - .isInstanceOf(IndexOutOfBoundsException.class); + .isThrownBy(() -> binder.bind(mpvs)) + .havingRootCause() + .isInstanceOf(IndexOutOfBoundsException.class); } @Test @@ -1909,13 +1970,13 @@ class DataBinderTests { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAutoGrowCollectionLimit(10); - MutablePropertyValues mpvs = new MutablePropertyValues(); mpvs.add("friends[16]", ""); + assertThatExceptionOfType(InvalidPropertyException.class) - .isThrownBy(() -> binder.bind(mpvs)) - .havingRootCause() - .isInstanceOf(IndexOutOfBoundsException.class); + .isThrownBy(() -> binder.bind(mpvs)) + .havingRootCause() + .isInstanceOf(IndexOutOfBoundsException.class); } @Test @@ -2165,6 +2226,7 @@ class DataBinderTests { } } + @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 09694c8e8b8..ed33f857b07 100644 --- a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -22,6 +22,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Unit tests for {@link ValidationUtils}. @@ -29,82 +30,93 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * @author Juergen Hoeller * @author Rick Evans * @author Chris Beams + * @author Arjen Poutsma * @since 08.10.2004 */ 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 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!")); - 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 { + public void testInvokeValidatorWithNullValidator() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new SimpleErrors(tb); assertThatIllegalArgumentException().isThrownBy(() -> ValidationUtils.invokeValidator(null, tb, errors)); } @Test - public void testInvokeValidatorWithNullErrors() throws Exception { + public void testInvokeValidatorWithNullErrors() { TestBean tb = new TestBean(); assertThatIllegalArgumentException().isThrownBy(() -> ValidationUtils.invokeValidator(emptyValidator, tb, null)); } @Test - public void testInvokeValidatorSunnyDay() throws Exception { + public void testInvokeValidatorSunnyDay() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new SimpleErrors(tb); ValidationUtils.invokeValidator(emptyValidator, tb, errors); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); } @Test - public void testValidationUtilsSunnyDay() throws Exception { + public void testValidationUtilsSunnyDay() { TestBean tb = new TestBean(""); tb.setName(" "); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - emptyValidator.validate(tb, errors); + Errors errors = emptyValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isFalse(); tb.setName("Roddy"); - errors = new BeanPropertyBindingResult(tb, "tb"); - emptyValidator.validate(tb, errors); + errors = emptyValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isFalse(); + + // Should not raise exception + errors.failOnError(IllegalStateException::new); } @Test - public void testValidationUtilsNull() throws Exception { + public void testValidationUtilsNull() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - emptyValidator.validate(tb, errors); + Errors errors = emptyValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); + + assertThatIllegalStateException() + .isThrownBy(() -> errors.failOnError(IllegalStateException::new)) + .withMessageContaining("'name'").withMessageContaining("EMPTY"); } @Test - public void testValidationUtilsEmpty() throws Exception { + public void testValidationUtilsEmpty() { TestBean tb = new TestBean(""); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - emptyValidator.validate(tb, errors); + Errors errors = emptyValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); + + assertThatIllegalStateException() + .isThrownBy(() -> errors.failOnError(IllegalStateException::new)) + .withMessageContaining("'name'").withMessageContaining("EMPTY"); } @Test public void testValidationUtilsEmptyVariants() { TestBean tb = new TestBean(); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new SimpleErrors(tb); ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); assertThat(errors.getFieldError("name").getArguments()[0]).isEqualTo("arg"); - errors = new BeanPropertyBindingResult(tb, "tb"); + errors = new SimpleErrors(tb); ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}, "msg"); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); @@ -113,33 +125,29 @@ public class ValidationUtilsTests { } @Test - public void testValidationUtilsEmptyOrWhitespace() throws Exception { + public void testValidationUtilsEmptyOrWhitespace() { TestBean tb = new TestBean(); // Test null - Errors errors = new BeanPropertyBindingResult(tb, "tb"); - emptyOrWhitespaceValidator.validate(tb, errors); + Errors errors = emptyOrWhitespaceValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test empty String tb.setName(""); - errors = new BeanPropertyBindingResult(tb, "tb"); - emptyOrWhitespaceValidator.validate(tb, errors); + errors = emptyOrWhitespaceValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test whitespace String tb.setName(" "); - errors = new BeanPropertyBindingResult(tb, "tb"); - emptyOrWhitespaceValidator.validate(tb, errors); + errors = emptyOrWhitespaceValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); // Test OK tb.setName("Roddy"); - errors = new BeanPropertyBindingResult(tb, "tb"); - emptyOrWhitespaceValidator.validate(tb, errors); + errors = emptyOrWhitespaceValidator.validateObject(tb); assertThat(errors.hasFieldErrors("name")).isFalse(); } @@ -148,13 +156,13 @@ public class ValidationUtilsTests { TestBean tb = new TestBean(); tb.setName(" "); - Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Errors errors = new SimpleErrors(tb); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); assertThat(errors.getFieldError("name").getArguments()[0]).isEqualTo("arg"); - errors = new BeanPropertyBindingResult(tb, "tb"); + errors = new SimpleErrors(tb); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}, "msg"); assertThat(errors.hasFieldErrors("name")).isTrue(); assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE");