Skip to content

Commit cc8361c

Browse files
committed
Add MethodValidationResult
See gh-29825
1 parent 8b53fec commit cc8361c

File tree

5 files changed

+170
-66
lines changed

5 files changed

+170
-66
lines changed

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

+50-13
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;
@@ -73,6 +74,8 @@ public class MethodValidationAdapter {
7374

7475
private static final Comparator<ParameterValidationResult> RESULT_COMPARATOR = new ResultComparator();
7576

77+
private static final MethodValidationResult EMPTY_RESULT = new EmptyMethodValidationResult();
78+
7679

7780
private final Supplier<Validator> validator;
7881

@@ -186,15 +189,19 @@ public static Class<?>[] determineValidationGroups(Object target, Method method)
186189
}
187190

188191
/**
189-
* Validate the given method arguments and raise {@link ConstraintViolation}
190-
* in case of any errors.
192+
* Validate the given method arguments and return the result of validation.
191193
* @param target the target Object
192194
* @param method the target method
193195
* @param arguments candidate arguments for a method invocation
194196
* @param groups groups for validation determined via
195197
* {@link #determineValidationGroups(Object, Method)}
198+
* @return a result with {@link ConstraintViolation violations} and
199+
* {@link ParameterValidationResult validationResults}, both possibly empty
200+
* in case there are no violations
196201
*/
197-
public void validateMethodArguments(Object target, Method method, Object[] arguments, Class<?>[] groups) {
202+
public MethodValidationResult validateMethodArguments(
203+
Object target, Method method, Object[] arguments, Class<?>[] groups) {
204+
198205
ExecutableValidator execVal = this.validator.get().forExecutables();
199206
Set<ConstraintViolation<Object>> result;
200207
try {
@@ -207,28 +214,26 @@ public void validateMethodArguments(Object target, Method method, Object[] argum
207214
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(mostSpecificMethod);
208215
result = execVal.validateParameters(target, bridgedMethod, arguments, groups);
209216
}
210-
if (!result.isEmpty()) {
211-
throw createException(target, method, result, i -> arguments[i]);
212-
}
217+
return (result.isEmpty() ? EMPTY_RESULT : createException(target, method, result, i -> arguments[i]));
213218
}
214219

215220
/**
216-
* Validate the given return value and raise {@link ConstraintViolation}
217-
* in case of any errors.
221+
* Validate the given return value and return the result of validation.
218222
* @param target the target Object
219223
* @param method the target method
220224
* @param returnValue value returned from invoking the target method
221225
* @param groups groups for validation determined via
222226
* {@link #determineValidationGroups(Object, Method)}
227+
* @return a result with {@link ConstraintViolation violations} and
228+
* {@link ParameterValidationResult validationResults}, both possibly empty
229+
* in case there are no violations
223230
*/
224-
public void validateMethodReturnValue(
231+
public MethodValidationResult validateMethodReturnValue(
225232
Object target, Method method, @Nullable Object returnValue, Class<?>[] groups) {
226233

227234
ExecutableValidator execVal = this.validator.get().forExecutables();
228235
Set<ConstraintViolation<Object>> result = execVal.validateReturnValue(target, method, returnValue, groups);
229-
if (!result.isEmpty()) {
230-
throw createException(target, method, result, i -> returnValue);
231-
}
236+
return (result.isEmpty() ? EMPTY_RESULT : createException(target, method, result, i -> returnValue));
232237
}
233238

234239
private MethodValidationException createException(
@@ -275,7 +280,7 @@ else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
275280
cascadedViolations.forEach((node, builder) -> validatonResultList.add(builder.build()));
276281
validatonResultList.sort(RESULT_COMPARATOR);
277282

278-
return new MethodValidationException(target, method, validatonResultList, violations);
283+
return new MethodValidationException(target, method, violations, validatonResultList);
279284
}
280285

281286
/**
@@ -470,4 +475,36 @@ private <E> int compareKeys(ParameterErrors errors1, ParameterErrors errors2) {
470475
}
471476
}
472477

478+
479+
/**
480+
* {@link MethodValidationResult} to use when there are no violations.
481+
*/
482+
private static final class EmptyMethodValidationResult implements MethodValidationResult {
483+
484+
@Override
485+
public Set<ConstraintViolation<?>> getConstraintViolations() {
486+
return Collections.emptySet();
487+
}
488+
489+
@Override
490+
public List<ParameterValidationResult> getAllValidationResults() {
491+
return Collections.emptyList();
492+
}
493+
494+
@Override
495+
public List<ParameterValidationResult> getValueResults() {
496+
return Collections.emptyList();
497+
}
498+
499+
@Override
500+
public List<ParameterErrors> getBeanResults() {
501+
return Collections.emptyList();
502+
}
503+
504+
@Override
505+
public void throwIfViolationsPresent() {
506+
}
507+
508+
}
509+
473510
}

Diff for: spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java

+24-32
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,15 @@
2323
import jakarta.validation.ConstraintViolation;
2424
import jakarta.validation.ConstraintViolationException;
2525

26+
import org.springframework.util.Assert;
27+
2628
/**
27-
* Extension of {@link ConstraintViolationException} that exposes an additional
28-
* list of {@link ParameterValidationResult} with violations adapted to
29+
* Extension of {@link ConstraintViolationException} that implements
30+
* {@link MethodValidationResult} exposing an additional list of
31+
* {@link ParameterValidationResult} that represents violations adapted to
2932
* {@link org.springframework.context.MessageSourceResolvable} and grouped by
3033
* method parameter.
3134
*
32-
* <p>For {@link jakarta.validation.Valid @Valid}-annotated, Object method
33-
* parameters or return types with cascaded violations, the {@link ParameterErrors}
34-
* subclass of {@link ParameterValidationResult} implements
35-
* {@link org.springframework.validation.Errors} and exposes
36-
* {@link org.springframework.validation.FieldError field errors}.
3735
*
3836
* @author Rossen Stoyanchev
3937
* @since 6.1
@@ -42,7 +40,7 @@
4240
* @see MethodValidationAdapter
4341
*/
4442
@SuppressWarnings("serial")
45-
public class MethodValidationException extends ConstraintViolationException {
43+
public class MethodValidationException extends ConstraintViolationException implements MethodValidationResult {
4644

4745
private final Object target;
4846

@@ -52,11 +50,11 @@ public class MethodValidationException extends ConstraintViolationException {
5250

5351

5452
public MethodValidationException(
55-
Object target, Method method,
56-
List<ParameterValidationResult> validationResults,
57-
Set<? extends ConstraintViolation<?>> violations) {
53+
Object target, Method method, Set<? extends ConstraintViolation<?>> violations,
54+
List<ParameterValidationResult> validationResults) {
5855

5956
super(violations);
57+
Assert.notEmpty(violations, "'violations' must not be empty");
6058
this.target = target;
6159
this.method = method;
6260
this.allValidationResults = validationResults;
@@ -77,42 +75,36 @@ public Method getMethod() {
7775
return this.method;
7876
}
7977

80-
/**
81-
* Return all validation results. This includes method parameters with
82-
* constraints declared on them, as well as
83-
* {@link jakarta.validation.Valid @Valid} method parameters with
84-
* cascaded constraints.
85-
* @see #getValueResults()
86-
* @see #getBeanResults()
87-
*/
78+
// re-declare parent class method for NonNull treatment of interface
79+
80+
@Override
81+
public Set<ConstraintViolation<?>> getConstraintViolations() {
82+
return super.getConstraintViolations();
83+
}
84+
85+
@Override
8886
public List<ParameterValidationResult> getAllValidationResults() {
8987
return this.allValidationResults;
9088
}
9189

92-
/**
93-
* Return only validation results for method parameters with constraints
94-
* declared directly on them. This excludes
95-
* {@link jakarta.validation.Valid @Valid} method parameters with cascaded
96-
* constraints.
97-
* @see #getAllValidationResults()
98-
*/
90+
@Override
9991
public List<ParameterValidationResult> getValueResults() {
10092
return this.allValidationResults.stream()
10193
.filter(result -> !(result instanceof ParameterErrors))
10294
.toList();
10395
}
10496

105-
/**
106-
* Return only validation results for {@link jakarta.validation.Valid @Valid}
107-
* method parameters with cascaded constraints. This excludes method
108-
* parameters with constraints declared directly on them.
109-
* @see #getAllValidationResults()
110-
*/
97+
@Override
11198
public List<ParameterErrors> getBeanResults() {
11299
return this.allValidationResults.stream()
113100
.filter(result -> result instanceof ParameterErrors)
114101
.map(result -> (ParameterErrors) result)
115102
.toList();
116103
}
117104

105+
@Override
106+
public void throwIfViolationsPresent() {
107+
throw this;
108+
}
109+
118110
}

Diff for: spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,15 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
103103
Object target = getTarget(invocation);
104104
Method method = invocation.getMethod();
105105
Class<?>[] groups = determineValidationGroups(invocation);
106-
this.delegate.validateMethodArguments(target, method, invocation.getArguments(), groups);
106+
107+
this.delegate.validateMethodArguments(target, method, invocation.getArguments(), groups)
108+
.throwIfViolationsPresent();
107109

108110
Object returnValue = invocation.proceed();
109111

110-
this.delegate.validateMethodReturnValue(target, method, returnValue, groups);
112+
this.delegate.validateMethodReturnValue(target, method, returnValue, groups)
113+
.throwIfViolationsPresent();
114+
111115
return returnValue;
112116
}
113117

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.validation.beanvalidation;
18+
19+
import java.util.List;
20+
import java.util.Set;
21+
22+
import jakarta.validation.ConstraintViolation;
23+
24+
/**
25+
* Container for method validation results where underlying
26+
* {@link ConstraintViolation violations} have been adapted to
27+
* {@link ParameterValidationResult} each containing a list of
28+
* {@link org.springframework.context.MessageSourceResolvable} grouped by method
29+
* parameter.
30+
*
31+
* <p>For {@link jakarta.validation.Valid @Valid}-annotated, Object method
32+
* parameters or return types with cascaded violations, the {@link ParameterErrors}
33+
* subclass of {@link ParameterValidationResult} implements
34+
* {@link org.springframework.validation.Errors} and exposes
35+
* {@link org.springframework.validation.FieldError field errors}.
36+
*
37+
* @author Rossen Stoyanchev
38+
* @since 6.1
39+
*/
40+
public interface MethodValidationResult {
41+
42+
/**
43+
* Returns the set of constraint violations reported during a validation.
44+
* @return the {@code Set} of {@link ConstraintViolation}s, or an empty Set
45+
*/
46+
Set<ConstraintViolation<?>> getConstraintViolations();
47+
48+
/**
49+
* Return all validation results. This includes method parameters with
50+
* constraints declared on them, as well as
51+
* {@link jakarta.validation.Valid @Valid} method parameters with
52+
* cascaded constraints.
53+
* @see #getValueResults()
54+
* @see #getBeanResults()
55+
*/
56+
List<ParameterValidationResult> getAllValidationResults();
57+
58+
/**
59+
* Return only validation results for method parameters with constraints
60+
* declared directly on them. This excludes
61+
* {@link jakarta.validation.Valid @Valid} method parameters with cascaded
62+
* constraints.
63+
* @see #getAllValidationResults()
64+
*/
65+
List<ParameterValidationResult> getValueResults();
66+
67+
/**
68+
* Return only validation results for {@link jakarta.validation.Valid @Valid}
69+
* method parameters with cascaded constraints. This excludes method
70+
* parameters with constraints declared directly on them.
71+
* @see #getAllValidationResults()
72+
*/
73+
List<ParameterErrors> getBeanResults();
74+
75+
/**
76+
* Check if {@link #getConstraintViolations()} is empty, and if not, raise
77+
* {@link MethodValidationException}.
78+
* @throws MethodValidationException if the result contains any violations
79+
*/
80+
void throwIfViolationsPresent();
81+
82+
}

Diff for: spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java

+8-19
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@
3232
import org.springframework.validation.FieldError;
3333

3434
import static org.assertj.core.api.Assertions.assertThat;
35-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3635

3736
/**
3837
* Unit tests for {@link MethodValidationAdapter}.
3938
* @author Rossen Stoyanchev
4039
*/
4140
public class MethodValidationAdapterTests {
4241

42+
private static final MethodValidationAdapter validationAdapter = new MethodValidationAdapter();
43+
4344
private static final Person faustino1234 = new Person("Faustino1234");
4445

4546
private static final Person cayetana6789 = new Person("Cayetana6789");
@@ -154,23 +155,17 @@ void validateListArgument() {
154155
}
155156

156157
private void validateArguments(
157-
Object target, Method method, Object[] arguments, Consumer<MethodValidationException> assertions) {
158-
159-
MethodValidationAdapter adapter = new MethodValidationAdapter();
158+
Object target, Method method, Object[] arguments, Consumer<MethodValidationResult> assertions) {
160159

161-
assertThatExceptionOfType(MethodValidationException.class)
162-
.isThrownBy(() -> adapter.validateMethodArguments(target, method, arguments, new Class<?>[0]))
163-
.satisfies(assertions);
160+
assertions.accept(
161+
validationAdapter.validateMethodArguments(target, method, arguments, new Class<?>[0]));
164162
}
165163

166164
private void validateReturnValue(
167-
Object target, Method method, @Nullable Object returnValue, Consumer<MethodValidationException> assertions) {
168-
169-
MethodValidationAdapter adapter = new MethodValidationAdapter();
165+
Object target, Method method, @Nullable Object returnValue, Consumer<MethodValidationResult> assertions) {
170166

171-
assertThatExceptionOfType(MethodValidationException.class)
172-
.isThrownBy(() -> adapter.validateMethodReturnValue(target, method, returnValue, new Class<?>[0]))
173-
.satisfies(assertions);
167+
assertions.accept(
168+
validationAdapter.validateMethodReturnValue(target, method, returnValue, new Class<?>[0]));
174169
}
175170

176171
private static void assertBeanResult(
@@ -225,12 +220,6 @@ public void addPeople(@Valid List<Person> people) {
225220

226221
@SuppressWarnings("unused")
227222
private record Person(@Size(min = 1, max = 10) String name) {
228-
229-
@Override
230-
public String name() {
231-
return this.name;
232-
}
233-
234223
}
235224

236225
}

0 commit comments

Comments
 (0)