Skip to content

Commit b61eee7

Browse files
committed
Support cross-parameter validation
Closes gh-33271
1 parent 0d64c90 commit b61eee7

File tree

11 files changed

+187
-39
lines changed

11 files changed

+187
-39
lines changed

spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.reflect.Method;
2020
import java.util.ArrayList;
21+
import java.util.Collections;
2122
import java.util.Comparator;
2223
import java.util.Iterator;
2324
import java.util.LinkedHashMap;
@@ -301,6 +302,7 @@ private MethodValidationResult adaptViolations(
301302

302303
Map<Path.Node, ParamValidationResultBuilder> paramViolations = new LinkedHashMap<>();
303304
Map<Path.Node, ParamErrorsBuilder> nestedViolations = new LinkedHashMap<>();
305+
List<MessageSourceResolvable> crossParamErrors = null;
304306

305307
for (ConstraintViolation<Object> violation : violations) {
306308
Iterator<Path.Node> nodes = violation.getPropertyPath().iterator();
@@ -315,6 +317,11 @@ private MethodValidationResult adaptViolations(
315317
else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
316318
parameter = parameterFunction.apply(-1);
317319
}
320+
else if (node.getKind().equals(ElementKind.CROSS_PARAMETER)) {
321+
crossParamErrors = (crossParamErrors != null ? crossParamErrors : new ArrayList<>());
322+
crossParamErrors.add(createCrossParamError(target, method, violation));
323+
break;
324+
}
318325
else {
319326
continue;
320327
}
@@ -382,7 +389,8 @@ else if (arg instanceof Optional<?> optional) {
382389
nestedViolations.forEach((key, builder) -> resultList.add(builder.build()));
383390
resultList.sort(resultComparator);
384391

385-
return MethodValidationResult.create(target, method, resultList);
392+
return MethodValidationResult.create(target, method, resultList,
393+
(crossParamErrors != null ? crossParamErrors : Collections.emptyList()));
386394
}
387395

388396
private MethodParameter initMethodParameter(Method method, int index) {
@@ -413,6 +421,19 @@ private BindingResult createBindingResult(MethodParameter parameter, @Nullable O
413421
return result;
414422
}
415423

424+
private MessageSourceResolvable createCrossParamError(
425+
Object target, Method method, ConstraintViolation<Object> violation) {
426+
427+
String objectName = Conventions.getVariableName(target) + "#" + method.getName();
428+
429+
ConstraintDescriptor<?> descriptor = violation.getConstraintDescriptor();
430+
String code = descriptor.getAnnotation().annotationType().getSimpleName();
431+
String[] codes = this.messageCodesResolver.resolveMessageCodes(code, objectName);
432+
Object[] arguments = this.validatorAdapter.get().getArgumentsForConstraint(objectName, "", descriptor);
433+
434+
return new ViolationMessageSourceResolvable(codes, arguments, violation.getMessage(), violation);
435+
}
436+
416437

417438
/**
418439
* Strategy to resolve the name of an {@code @Valid} method parameter to

spring-context/src/main/java/org/springframework/validation/method/DefaultMethodValidationResult.java

+20-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.lang.reflect.Method;
2020
import java.util.List;
2121

22+
import org.springframework.context.MessageSourceResolvable;
2223
import org.springframework.util.Assert;
2324

2425
/**
@@ -33,19 +34,26 @@ final class DefaultMethodValidationResult implements MethodValidationResult {
3334

3435
private final Method method;
3536

36-
private final List<ParameterValidationResult> allValidationResults;
37+
private final List<ParameterValidationResult> parameterValidationResults;
38+
39+
private final List<MessageSourceResolvable> crossParamResults;
3740

3841
private final boolean forReturnValue;
3942

4043

41-
DefaultMethodValidationResult(Object target, Method method, List<ParameterValidationResult> results) {
42-
Assert.notEmpty(results, "'results' is required and must not be empty");
44+
DefaultMethodValidationResult(
45+
Object target, Method method, List<ParameterValidationResult> results,
46+
List<MessageSourceResolvable> crossParamResults) {
47+
48+
Assert.isTrue(!results.isEmpty() || !crossParamResults.isEmpty(), "Expected validation results");
4349
Assert.notNull(target, "'target' is required");
4450
Assert.notNull(method, "Method is required");
51+
4552
this.target = target;
4653
this.method = method;
47-
this.allValidationResults = results;
48-
this.forReturnValue = (results.get(0).getMethodParameter().getParameterIndex() == -1);
54+
this.parameterValidationResults = results;
55+
this.crossParamResults = crossParamResults;
56+
this.forReturnValue = (!results.isEmpty() && results.get(0).getMethodParameter().getParameterIndex() == -1);
4957
}
5058

5159

@@ -65,10 +73,14 @@ public boolean isForReturnValue() {
6573
}
6674

6775
@Override
68-
public List<ParameterValidationResult> getAllValidationResults() {
69-
return this.allValidationResults;
76+
public List<ParameterValidationResult> getParameterValidationResults() {
77+
return this.parameterValidationResults;
7078
}
7179

80+
@Override
81+
public List<MessageSourceResolvable> getCrossParameterValidationResults() {
82+
return this.crossParamResults;
83+
}
7284

7385
@Override
7486
public String toString() {

spring-context/src/main/java/org/springframework/validation/method/EmptyMethodValidationResult.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222

23+
import org.springframework.context.MessageSourceResolvable;
24+
2325
/**
2426
* {@link MethodValidationResult} with an empty list of results.
2527
*
@@ -44,7 +46,12 @@ public boolean isForReturnValue() {
4446
}
4547

4648
@Override
47-
public List<ParameterValidationResult> getAllValidationResults() {
49+
public List<ParameterValidationResult> getParameterValidationResults() {
50+
return Collections.emptyList();
51+
}
52+
53+
@Override
54+
public List<MessageSourceResolvable> getCrossParameterValidationResults() {
4855
return Collections.emptyList();
4956
}
5057

spring-context/src/main/java/org/springframework/validation/method/MethodValidationException.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.lang.reflect.Method;
2020
import java.util.List;
2121

22+
import org.springframework.context.MessageSourceResolvable;
2223
import org.springframework.util.Assert;
2324

2425
/**
@@ -57,8 +58,13 @@ public boolean isForReturnValue() {
5758
}
5859

5960
@Override
60-
public List<ParameterValidationResult> getAllValidationResults() {
61-
return this.validationResult.getAllValidationResults();
61+
public List<ParameterValidationResult> getParameterValidationResults() {
62+
return this.validationResult.getParameterValidationResults();
63+
}
64+
65+
@Override
66+
public List<MessageSourceResolvable> getCrossParameterValidationResults() {
67+
return this.validationResult.getCrossParameterValidationResults();
6268
}
6369

6470
}

spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java

+47-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.validation.method;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.Collections;
2021
import java.util.List;
2122

2223
import org.springframework.context.MessageSourceResolvable;
@@ -55,54 +56,75 @@ public interface MethodValidationResult {
5556
* Whether the result contains any validation errors.
5657
*/
5758
default boolean hasErrors() {
58-
return !getAllValidationResults().isEmpty();
59+
return !getParameterValidationResults().isEmpty();
5960
}
6061

6162
/**
6263
* Return a single list with all errors from all validation results.
63-
* @see #getAllValidationResults()
64+
* @see #getParameterValidationResults()
6465
* @see ParameterValidationResult#getResolvableErrors()
6566
*/
6667
default List<? extends MessageSourceResolvable> getAllErrors() {
67-
return getAllValidationResults().stream()
68+
return getParameterValidationResults().stream()
6869
.flatMap(result -> result.getResolvableErrors().stream())
6970
.toList();
7071
}
7172

73+
/**
74+
* Return all validation results per method parameter, including both
75+
* {@link #getValueResults()} and {@link #getBeanResults()}.
76+
* <p>Use {@link #getCrossParameterValidationResults()} for access to errors
77+
* from cross-parameter validation.
78+
* @since 6.2
79+
* @see #getValueResults()
80+
* @see #getBeanResults()
81+
*/
82+
List<ParameterValidationResult> getParameterValidationResults();
83+
7284
/**
7385
* Return all validation results. This includes both method parameters with
7486
* errors directly on them, and Object method parameters with nested errors
7587
* on their fields and properties.
7688
* @see #getValueResults()
7789
* @see #getBeanResults()
90+
* @deprecated deprecated in favor of {@link #getParameterValidationResults()}
7891
*/
79-
List<ParameterValidationResult> getAllValidationResults();
92+
@Deprecated(since = "6.2", forRemoval = true)
93+
default List<ParameterValidationResult> getAllValidationResults() {
94+
return getParameterValidationResults();
95+
}
8096

8197
/**
82-
* Return the subset of {@link #getAllValidationResults() allValidationResults}
98+
* Return the subset of {@link #getParameterValidationResults() allValidationResults}
8399
* that includes method parameters with validation errors directly on method
84100
* argument values. This excludes {@link #getBeanResults() beanResults} with
85101
* nested errors on their fields and properties.
86102
*/
87103
default List<ParameterValidationResult> getValueResults() {
88-
return getAllValidationResults().stream()
104+
return getParameterValidationResults().stream()
89105
.filter(result -> !(result instanceof ParameterErrors))
90106
.toList();
91107
}
92108

93109
/**
94-
* Return the subset of {@link #getAllValidationResults() allValidationResults}
110+
* Return the subset of {@link #getParameterValidationResults() allValidationResults}
95111
* that includes Object method parameters with nested errors on their fields
96112
* and properties. This excludes {@link #getValueResults() valueResults} with
97113
* validation errors directly on method arguments.
98114
*/
99115
default List<ParameterErrors> getBeanResults() {
100-
return getAllValidationResults().stream()
116+
return getParameterValidationResults().stream()
101117
.filter(ParameterErrors.class::isInstance)
102118
.map(result -> (ParameterErrors) result)
103119
.toList();
104120
}
105121

122+
/**
123+
* Return errors from cross-parameter validation.
124+
* @since 6.2
125+
*/
126+
List<MessageSourceResolvable> getCrossParameterValidationResults();
127+
106128

107129
/**
108130
* Factory method to create a {@link MethodValidationResult} instance.
@@ -112,7 +134,23 @@ default List<ParameterErrors> getBeanResults() {
112134
* @return the created instance
113135
*/
114136
static MethodValidationResult create(Object target, Method method, List<ParameterValidationResult> results) {
115-
return new DefaultMethodValidationResult(target, method, results);
137+
return create(target, method, results, Collections.emptyList());
138+
}
139+
140+
/**
141+
* Factory method to create a {@link MethodValidationResult} instance.
142+
* @param target the target Object
143+
* @param method the target method
144+
* @param results method validation results, expected to be non-empty
145+
* @param crossParameterErrors cross-parameter validation errors
146+
* @return the created instance
147+
* @since 6.2
148+
*/
149+
static MethodValidationResult create(
150+
Object target, Method method, List<ParameterValidationResult> results,
151+
List<MessageSourceResolvable> crossParameterErrors) {
152+
153+
return new DefaultMethodValidationResult(target, method, results, crossParameterErrors);
116154
}
117155

118156
/**

0 commit comments

Comments
 (0)