diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index a472531bfb..a15f0726fd 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -301,8 +301,8 @@ public class MethodValidationAdapter implements MethodValidator { Function parameterFunction, Function argumentFunction) { - Map parameterViolations = new LinkedHashMap<>(); - Map cascadedViolations = new LinkedHashMap<>(); + Map paramViolations = new LinkedHashMap<>(); + Map beanViolations = new LinkedHashMap<>(); for (ConstraintViolation violation : violations) { Iterator itr = violation.getPropertyPath().iterator(); @@ -321,28 +321,29 @@ public class MethodValidationAdapter implements MethodValidator { continue; } - Object argument = argumentFunction.apply(parameter.getParameterIndex()); + Object arg = argumentFunction.apply(parameter.getParameterIndex()); if (!itr.hasNext()) { - parameterViolations - .computeIfAbsent(parameter, p -> new ValueResultBuilder(target, parameter, argument)) + paramViolations + .computeIfAbsent(parameter, p -> new ParamResultBuilder(target, parameter, arg)) .addViolation(violation); } else { - cascadedViolations - .computeIfAbsent(new CascadedViolationsKey(node, violation.getLeafBean()), - n -> new BeanResultBuilder(parameter, argument, itr.next(), violation.getLeafBean())) + Object leafBean = violation.getLeafBean(); + BeanResultKey key = new BeanResultKey(node, leafBean); + beanViolations + .computeIfAbsent(key, k -> new BeanResultBuilder(parameter, arg, itr.next(), leafBean)) .addViolation(violation); } break; } } - List validatonResultList = new ArrayList<>(); - parameterViolations.forEach((parameter, builder) -> validatonResultList.add(builder.build())); - cascadedViolations.forEach((violationsKey, builder) -> validatonResultList.add(builder.build())); - validatonResultList.sort(resultComparator); + List resultList = new ArrayList<>(); + paramViolations.forEach((param, builder) -> resultList.add(builder.build())); + beanViolations.forEach((key, builder) -> resultList.add(builder.build())); + resultList.sort(resultComparator); - return MethodValidationResult.create(target, method, validatonResultList); + return MethodValidationResult.create(target, method, resultList); } private MethodParameter initMethodParameter(Method method, int index) { @@ -373,14 +374,6 @@ public class MethodValidationAdapter implements MethodValidator { return result; } - /** - * A unique key for the cascaded violations map. Individually, the node and leaf bean may not be unique for all - * collection types ({@link Set} will have the same node and {@link List} may have the same leaf), but together - * they should represent a distinct pairing. - * @param node the path of the violation - * @param leafBean the validated object - */ - record CascadedViolationsKey(Path.Node node, Object leafBean) { } /** * Strategy to resolve the name of an {@code @Valid} method parameter to @@ -403,7 +396,7 @@ public class MethodValidationAdapter implements MethodValidator { * Builds a validation result for a value method parameter with constraints * declared directly on it. */ - private final class ValueResultBuilder { + private final class ParamResultBuilder { private final Object target; @@ -414,7 +407,7 @@ public class MethodValidationAdapter implements MethodValidator { private final List resolvableErrors = new ArrayList<>(); - public ValueResultBuilder(Object target, MethodParameter parameter, @Nullable Object argument) { + public ParamResultBuilder(Object target, MethodParameter parameter, @Nullable Object argument) { this.target = target; this.parameter = parameter; this.argument = argument; @@ -440,7 +433,7 @@ public class MethodValidationAdapter implements MethodValidator { private final MethodParameter parameter; @Nullable - private final Object argument; + private final Object bean; @Nullable private final Object container; @@ -455,20 +448,13 @@ public class MethodValidationAdapter implements MethodValidator { private final Set> violations = new LinkedHashSet<>(); - public BeanResultBuilder(MethodParameter parameter, @Nullable Object argument, Path.Node node, @Nullable Object leafBean) { - this.parameter = parameter; - + public BeanResultBuilder(MethodParameter param, @Nullable Object arg, Path.Node node, @Nullable Object leafBean) { + this.parameter = param; + this.bean = leafBean; + this.container = (arg != null && !arg.equals(leafBean) ? arg : null); this.containerIndex = node.getIndex(); this.containerKey = node.getKey(); - if (argument != null && !argument.equals(leafBean)) { - this.container = argument; - } - else { - this.container = null; - } - - this.argument = leafBean; - this.errors = createBindingResult(parameter, leafBean); + this.errors = createBindingResult(param, leafBean); } public void addViolation(ConstraintViolation violation) { @@ -478,12 +464,28 @@ public class MethodValidationAdapter implements MethodValidator { public ParameterErrors build() { validatorAdapter.get().processConstraintViolations(this.violations, this.errors); return new ParameterErrors( - this.parameter, this.argument, this.errors, this.container, + this.parameter, this.bean, this.errors, this.container, this.containerIndex, this.containerKey); } } + /** + * Unique key for cascaded violations associated with a bean. + *

The bean may be an element within a container such as a List, Set, array, + * Map, Optional, and others. In that case the {@link Path.Node} represents + * the container element with its index or key, if applicable, while the + * {@link ConstraintViolation#getLeafBean() leafBean} is the actual + * element instance. The pair should be unique. For example in a Set, the + * node is the same but element instances are unique. In a List or Map the + * node is further qualified by an index or key while element instances + * may be the same. + * @param node the path to the bean associated with the violation + * @param leafBean the bean instance + */ + record BeanResultKey(Path.Node node, Object leafBean) { } + + /** * Default algorithm to select an object name, as described in * {@link #setObjectNameResolver(ObjectNameResolver)}. diff --git a/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java b/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java index 3f317cdaf8..e601942bf2 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java @@ -32,10 +32,12 @@ import org.springframework.validation.ObjectError; * {@link Errors#getAllErrors()}, but this subclass provides access to the same * as {@link FieldError}s. * - *

When the method parameter is a multi-element container like {@link List} or - * {@link java.util.Map}, a separate {@link ParameterErrors} is created for each - * value for which there are validation errors. Otherwise, only a single - * {@link ParameterErrors} will be created. + *

When the method parameter is a container with multiple elements such as a + * {@link List}, {@link java.util.Set}, array, {@link java.util.Map}, or others, + * then a separate {@link ParameterErrors} is created for each element that has + * errors. In that case, the {@link #getContainer() container}, + * {@link #getContainerIndex() containerIndex}, and {@link #getContainerKey() containerKey} + * provide additional context. * * @author Rossen Stoyanchev * @since 6.1 @@ -70,12 +72,12 @@ public class ParameterErrors extends ParameterValidationResult implements Errors /** - * When {@code @Valid} is declared on a container type method parameter such as - * {@link java.util.Collection}, {@link java.util.Optional} or {@link java.util.Map}, - * this method returns the parent that contained the validated object - * {@link #getArgument() argument}, while {@link #getContainerIndex()} and - * {@link #getContainerKey()} returns the respective index or key if the parameter's - * datatype supports such access. + * When {@code @Valid} is declared on a container of elements such as + * {@link java.util.Collection}, {@link java.util.Map}, + * {@link java.util.Optional}, and others, this method returns the container + * of the validated {@link #getArgument() argument}, while + * {@link #getContainerIndex()} and {@link #getContainerKey()} provide + * information about the index or key if applicable. */ @Nullable public Object getContainer() { @@ -83,10 +85,9 @@ public class ParameterErrors extends ParameterValidationResult implements Errors } /** - * When {@code @Valid} is declared on an indexed type, such as {@link List}, - * this method returns the index under which the validated object - * {@link #getArgument() argument} is stored in the list - * {@link #getContainer() container}. + * When {@code @Valid} is declared on an indexed container of elements such as + * {@link List} or array, this method returns the index of the validated + * {@link #getArgument() argument}. */ @Nullable public Integer getContainerIndex() { @@ -94,9 +95,9 @@ public class ParameterErrors extends ParameterValidationResult implements Errors } /** - * When {@code @Valid} is declared on a keyed typed, such as {@link java.util.Map}, - * this method returns the key under which the validated object {@link #getArgument() - * argument} is stored in the map {@link #getContainer()}. + * When {@code @Valid} is declared on a container of elements referenced by + * key such as {@link java.util.Map}, this method returns the key of the + * validated {@link #getArgument() argument}. */ @Nullable public Object getContainerKey() {