From 391d7f2c6a1a66be70333a7b0717e9498f5f8927 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Jul 2023 22:47:20 +0200 Subject: [PATCH 1/2] Polishing --- .../validation/AbstractBindingResult.java | 4 +- .../validation/AbstractErrors.java | 19 +- .../validation/BeanPropertyBindingResult.java | 6 +- .../validation/BindException.java | 6 +- .../validation/DirectFieldBindingResult.java | 6 +- .../springframework/validation/Errors.java | 82 +++--- .../springframework/validation/Validator.java | 19 +- .../validation/DataBinderTests.java | 237 +++++++++--------- .../validation/ValidationUtilsTests.java | 16 +- .../web/bind/EscapedErrors.java | 6 +- .../support/WebExchangeBindException.java | 4 +- 11 files changed, 219 insertions(+), 186 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java index c431cfddfc0..64b9c9b5fb1 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java @@ -102,8 +102,8 @@ public abstract class AbstractBindingResult extends AbstractErrors implements Bi } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, - @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { if (!StringUtils.hasLength(getNestedPath()) && !StringUtils.hasLength(field)) { // We're at the top of the nested object hierarchy, 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..b9e5617a53c 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); } } @@ -201,9 +202,9 @@ public abstract class AbstractErrors implements Errors, Serializable { 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); diff --git a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java index 8558a2619c9..1cc6bb8e365 100644 --- a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.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. @@ -55,7 +55,7 @@ public class BeanPropertyBindingResult extends AbstractPropertyBindingResult imp /** - * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * Create a new {@code BeanPropertyBindingResult} for the given target. * @param target the target bean to bind onto * @param objectName the name of the target object */ @@ -64,7 +64,7 @@ public class BeanPropertyBindingResult extends AbstractPropertyBindingResult imp } /** - * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * Create a new {@code BeanPropertyBindingResult} for the given target. * @param target the target bean to bind onto * @param objectName the name of the target object * @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value diff --git a/spring-context/src/main/java/org/springframework/validation/BindException.java b/spring-context/src/main/java/org/springframework/validation/BindException.java index afc0c5c2ae7..b84c81081a8 100644 --- a/spring-context/src/main/java/org/springframework/validation/BindException.java +++ b/spring-context/src/main/java/org/springframework/validation/BindException.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. @@ -128,7 +128,9 @@ public class BindException extends Exception implements BindingResult { } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); } diff --git a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java index 5ad401de5be..f01232ca1f6 100644 --- a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.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. @@ -46,7 +46,7 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult { /** - * Create a new DirectFieldBindingResult instance. + * Create a new {@code DirectFieldBindingResult} for the given target. * @param target the target object to bind onto * @param objectName the name of the target object */ @@ -55,7 +55,7 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult { } /** - * Create a new DirectFieldBindingResult instance. + * Create a new {@code DirectFieldBindingResult} for the given target. * @param target the target object to bind onto * @param objectName the name of the target object * @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value 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..3759217b834 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. @@ -22,24 +22,24 @@ 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 BindException + * @see BindingResult */ public interface Errors { @@ -66,6 +66,7 @@ public interface Errors { * @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); @@ -73,6 +74,7 @@ public interface Errors { * 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(); @@ -86,14 +88,14 @@ public interface Errors { *

For example: current path "spouse.", pushNestedPath("child") → * result path "spouse.child."; popNestedPath() → "spouse." again. * @param subPath the sub path to push onto the nested path stack - * @see #popNestedPath + * @see #popNestedPath() */ void pushNestedPath(String subPath); /** * 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; @@ -101,6 +103,7 @@ public interface Errors { * 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); @@ -109,6 +112,7 @@ public interface Errors { * 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); @@ -119,6 +123,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,7 +137,7 @@ 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); @@ -147,7 +152,7 @@ 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); @@ -164,7 +169,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); @@ -179,35 +184,40 @@ public interface Errors { * to refer to the same target object, or at least contain compatible errors * that apply to the target object of this {@code Errors} instance. * @param errors the {@code Errors} instance to merge in + * @see #getAllErrors() */ void addAllErrors(Errors errors); /** - * Return if there were any errors. + * Determine if there were any errors. + * @see #hasGlobalErrors() + * @see #hasFieldErrors() */ boolean hasErrors(); /** - * Return the total number of errors. + * Determine the total number of errors. + * @see #getGlobalErrorCount() + * @see #getFieldErrorCount() */ int getErrorCount(); /** * Get all errors, both global and field ones. - * @return a list of {@link ObjectError} instances + * @return a list of {@link ObjectError}/{@link FieldError} instances + * @see #getGlobalErrors() + * @see #getFieldErrors() */ List getAllErrors(); /** - * Are there any global errors? - * @return {@code true} if there are any global errors + * Determine if there were any global errors. * @see #hasFieldErrors() */ boolean hasGlobalErrors(); /** - * Return the number of global errors. - * @return the number of global errors + * Determine the number of global errors. * @see #getFieldErrorCount() */ int getGlobalErrorCount(); @@ -215,26 +225,26 @@ public interface Errors { /** * 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(); /** - * Are there any field errors? - * @return {@code true} if there are any errors associated with a field + * Determine if there were any errors associated with a field. * @see #hasGlobalErrors() */ boolean hasFieldErrors(); /** - * Return the number of errors associated with a field. - * @return the number of errors associated with a field + * Determine the number of errors associated with a field. * @see #getGlobalErrorCount() */ int getFieldErrorCount(); @@ -242,36 +252,39 @@ public interface Errors { /** * 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(); /** - * Are there any errors associated with the given field? + * Determine if there were 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); /** * 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); /** * 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); @@ -279,6 +292,7 @@ public interface Errors { * 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); @@ -290,6 +304,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,6 +316,7 @@ 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); 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 b67b6d5d8b7..2aa28239635 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,14 +54,14 @@ package org.springframework.validation; * } * } * - *

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 * @see SmartValidator * @see Errors * @see ValidationUtils + * @see DataBinder#setValidator */ public interface Validator { @@ -81,11 +81,14 @@ 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 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 a990612917e..037dc8d214a 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -157,8 +157,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 @@ -168,8 +169,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 @@ -197,57 +199,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 @@ -257,15 +258,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 { @@ -286,6 +289,7 @@ class DataBinderTests { return ((TestBean) getValue()).getName(); } }); + MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("age", "32x"); @@ -293,41 +297,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 @@ -1134,12 +1136,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."); @@ -1187,8 +1188,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); @@ -1201,7 +1201,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"); @@ -1257,10 +1261,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); @@ -1273,7 +1278,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"); @@ -1327,9 +1336,11 @@ class DataBinderTests { @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."); Validator spouseValidator = new SpouseValidator(); @@ -1800,16 +1811,16 @@ class DataBinderTests { tb.setName("myName"); tb.setAge(99); - BeanPropertyBindingResult ex = new BeanPropertyBindingResult(tb, "tb"); - ex.reject("invalid"); - ex.rejectValue("age", "invalidField"); + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(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 @@ -1877,13 +1888,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 @@ -1904,13 +1915,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 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 0a027b95df4..1131a2d645c 100644 --- a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException public class ValidationUtilsTests { @Test - public void testInvokeValidatorWithNullValidator() throws Exception { + public void testInvokeValidatorWithNullValidator() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); assertThatIllegalArgumentException().isThrownBy(() -> @@ -43,14 +43,14 @@ public class ValidationUtilsTests { } @Test - public void testInvokeValidatorWithNullErrors() throws Exception { + public void testInvokeValidatorWithNullErrors() { TestBean tb = new TestBean(); assertThatIllegalArgumentException().isThrownBy(() -> ValidationUtils.invokeValidator(new EmptyValidator(), tb, null)); } @Test - public void testInvokeValidatorSunnyDay() throws Exception { + public void testInvokeValidatorSunnyDay() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); ValidationUtils.invokeValidator(new EmptyValidator(), tb, errors); @@ -59,7 +59,7 @@ public class ValidationUtilsTests { } @Test - public void testValidationUtilsSunnyDay() throws Exception { + public void testValidationUtilsSunnyDay() { TestBean tb = new TestBean(""); Validator testValidator = new EmptyValidator(); @@ -75,7 +75,7 @@ public class ValidationUtilsTests { } @Test - public void testValidationUtilsNull() throws Exception { + public void testValidationUtilsNull() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); Validator testValidator = new EmptyValidator(); @@ -85,7 +85,7 @@ public class ValidationUtilsTests { } @Test - public void testValidationUtilsEmpty() throws Exception { + public void testValidationUtilsEmpty() { TestBean tb = new TestBean(""); Errors errors = new BeanPropertyBindingResult(tb, "tb"); Validator testValidator = new EmptyValidator(); @@ -113,7 +113,7 @@ public class ValidationUtilsTests { } @Test - public void testValidationUtilsEmptyOrWhitespace() throws Exception { + public void testValidationUtilsEmptyOrWhitespace() { TestBean tb = new TestBean(); Validator testValidator = new EmptyOrWhitespaceValidator(); diff --git a/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java b/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java index eb1166f9ade..9edffefc90e 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java +++ b/spring-web/src/main/java/org/springframework/web/bind/EscapedErrors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,8 +110,8 @@ public class EscapedErrors implements Errors { } @Override - public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, - @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { this.source.rejectValue(field, errorCode, errorArgs, defaultMessage); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java index 1c1adcac725..28d81ef2fe2 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java @@ -122,8 +122,8 @@ public class WebExchangeBindException extends ServerWebInputException implements } @Override - public void rejectValue( - @Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); } From 8cc6dd629a0f564a7b528f3d3bfcc73a9833214b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Jul 2023 22:58:27 +0200 Subject: [PATCH 2/2] Polishing --- .../src/main/java/org/springframework/validation/Errors.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3759217b834..18a7bc1910a 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -272,7 +272,7 @@ public interface Errors { boolean hasFieldErrors(String field); /** - * Return the number of errors associated with the given field. + * Determine the number of errors associated with the given field. * @param field the field name * @see #getFieldErrorCount() */ @@ -310,7 +310,7 @@ public interface Errors { Object getFieldValue(String field); /** - * Return the type of a given field. + * Determine the type of the given field, as far as possible. *

Implementations should be able to determine the type even * when the field value is {@code null}, for example from some * associated descriptor.