Skip to content

Commit deaa493

Browse files
committed
Add Visitor to HandlerMethodValidationException
Closes gh-30813
1 parent 2a126fa commit deaa493

File tree

10 files changed

+497
-36
lines changed

10 files changed

+497
-36
lines changed

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ default List<? extends MessageSourceResolvable> getAllErrors() {
7979
List<ParameterValidationResult> getAllValidationResults();
8080

8181
/**
82-
* Return only validation results for method parameters with errors directly
83-
* on them. This does not include Object method parameters with nested
84-
* errors on their fields and properties.
82+
* Return the subset of {@link #getAllValidationResults() allValidationResults}
83+
* that includes method parameters with validation errors directly on method
84+
* argument values. This excludes {@link #getBeanResults() beanResults} with
85+
* nested errors on their fields and properties.
8586
*/
8687
default List<ParameterValidationResult> getValueResults() {
8788
return getAllValidationResults().stream()
@@ -90,9 +91,10 @@ default List<ParameterValidationResult> getValueResults() {
9091
}
9192

9293
/**
93-
* Return only validation results for Object method parameters with nested
94-
* errors on their fields and properties. This excludes method parameters
95-
* with errors directly on them.
94+
* Return the subset of {@link #getAllValidationResults() allValidationResults}
95+
* that includes Object method parameters with nested errors on their fields
96+
* and properties. This excludes {@link #getValueResults() valueResults} with
97+
* validation errors directly on method arguments.
9698
*/
9799
default List<ParameterErrors> getBeanResults() {
98100
return getAllValidationResults().stream()

spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,24 @@
1919
import java.lang.reflect.Method;
2020
import java.util.List;
2121
import java.util.Locale;
22+
import java.util.function.Predicate;
2223

2324
import org.springframework.context.MessageSource;
25+
import org.springframework.core.MethodParameter;
2426
import org.springframework.http.HttpStatus;
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.util.Assert;
2529
import org.springframework.validation.method.MethodValidationResult;
30+
import org.springframework.validation.method.ParameterErrors;
2631
import org.springframework.validation.method.ParameterValidationResult;
32+
import org.springframework.web.bind.annotation.CookieValue;
33+
import org.springframework.web.bind.annotation.MatrixVariable;
34+
import org.springframework.web.bind.annotation.ModelAttribute;
35+
import org.springframework.web.bind.annotation.PathVariable;
36+
import org.springframework.web.bind.annotation.RequestBody;
37+
import org.springframework.web.bind.annotation.RequestHeader;
38+
import org.springframework.web.bind.annotation.RequestParam;
39+
import org.springframework.web.bind.annotation.RequestPart;
2740
import org.springframework.web.server.ResponseStatusException;
2841
import org.springframework.web.util.BindErrorUtils;
2942

@@ -43,10 +56,24 @@ public class HandlerMethodValidationException extends ResponseStatusException im
4356

4457
private final MethodValidationResult validationResult;
4558

59+
private final Predicate<MethodParameter> modelAttribitePredicate;
60+
61+
private final Predicate<MethodParameter> requestParamPredicate;
62+
4663

4764
public HandlerMethodValidationException(MethodValidationResult validationResult) {
65+
this(validationResult,
66+
param -> param.hasParameterAnnotation(ModelAttribute.class),
67+
param -> param.hasParameterAnnotation(RequestParam.class));
68+
}
69+
70+
public HandlerMethodValidationException(MethodValidationResult validationResult,
71+
Predicate<MethodParameter> modelAttribitePredicate, Predicate<MethodParameter> requestParamPredicate) {
72+
4873
super(initHttpStatus(validationResult), "Validation failure", null, null, null);
4974
this.validationResult = validationResult;
75+
this.modelAttribitePredicate = modelAttribitePredicate;
76+
this.requestParamPredicate = requestParamPredicate;
5077
}
5178

5279
private static HttpStatus initHttpStatus(MethodValidationResult validationResult) {
@@ -84,4 +111,133 @@ public List<ParameterValidationResult> getAllValidationResults() {
84111
return this.validationResult.getAllValidationResults();
85112
}
86113

114+
/**
115+
* Provide a {@link Visitor Visitor} to handle {@link ParameterValidationResult}s
116+
* through callback methods organized by controller method parameter type.
117+
*/
118+
public void visitResults(Visitor visitor) {
119+
for (ParameterValidationResult result : getAllValidationResults()) {
120+
MethodParameter param = result.getMethodParameter();
121+
CookieValue cookieValue = param.getParameterAnnotation(CookieValue.class);
122+
if (cookieValue != null) {
123+
visitor.cookieValue(cookieValue, result);
124+
continue;
125+
}
126+
MatrixVariable matrixVariable = param.getParameterAnnotation(MatrixVariable.class);
127+
if (matrixVariable != null) {
128+
visitor.matrixVariable(matrixVariable, result);
129+
continue;
130+
}
131+
if (this.modelAttribitePredicate.test(param)) {
132+
ModelAttribute modelAttribute = param.getParameterAnnotation(ModelAttribute.class);
133+
visitor.modelAttribute(modelAttribute, asErrors(result));
134+
continue;
135+
}
136+
PathVariable pathVariable = param.getParameterAnnotation(PathVariable.class);
137+
if (pathVariable != null) {
138+
visitor.pathVariable(pathVariable, result);
139+
continue;
140+
}
141+
RequestBody requestBody = param.getParameterAnnotation(RequestBody.class);
142+
if (requestBody != null) {
143+
visitor.requestBody(requestBody, asErrors(result));
144+
continue;
145+
}
146+
RequestHeader requestHeader = param.getParameterAnnotation(RequestHeader.class);
147+
if (requestHeader != null) {
148+
visitor.requestHeader(requestHeader, result);
149+
continue;
150+
}
151+
if (this.requestParamPredicate.test(param)) {
152+
RequestParam requestParam = param.getParameterAnnotation(RequestParam.class);
153+
visitor.requestParam(requestParam, result);
154+
continue;
155+
}
156+
RequestPart requestPart = param.getParameterAnnotation(RequestPart.class);
157+
if (requestPart != null) {
158+
visitor.requestPart(requestPart, asErrors(result));
159+
continue;
160+
}
161+
visitor.other(result);
162+
}
163+
}
164+
165+
private static ParameterErrors asErrors(ParameterValidationResult result) {
166+
Assert.state(result instanceof ParameterErrors, "Expected ParameterErrors");
167+
return (ParameterErrors) result;
168+
}
169+
170+
171+
/**
172+
* Contract to handle validation results with callbacks by controller method
173+
* parameter type, with {@link #other} serving as the fallthrough.
174+
*/
175+
public interface Visitor {
176+
177+
/**
178+
* Handle results for {@code @CookieValue} method parameters.
179+
* @param cookieValue the annotation declared on the parameter
180+
* @param result the validation result
181+
*/
182+
void cookieValue(CookieValue cookieValue, ParameterValidationResult result);
183+
184+
/**
185+
* Handle results for {@code @MatrixVariable} method parameters.
186+
* @param matrixVariable the annotation declared on the parameter
187+
* @param result the validation result
188+
*/
189+
void matrixVariable(MatrixVariable matrixVariable, ParameterValidationResult result);
190+
191+
/**
192+
* Handle results for {@code @ModelAttribute} method parameters.
193+
* @param modelAttribute the optional {@code ModelAttribute} annotation,
194+
* possibly {@code null} if the method parameter is declared without it.
195+
* @param errors the validation errors
196+
*/
197+
void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors);
198+
199+
/**
200+
* Handle results for {@code @PathVariable} method parameters.
201+
* @param pathVariable the annotation declared on the parameter
202+
* @param result the validation result
203+
*/
204+
void pathVariable(PathVariable pathVariable, ParameterValidationResult result);
205+
206+
/**
207+
* Handle results for {@code @RequestBody} method parameters.
208+
* @param requestBody the annotation declared on the parameter
209+
* @param errors the validation error
210+
*/
211+
void requestBody(RequestBody requestBody, ParameterErrors errors);
212+
213+
/**
214+
* Handle results for {@code @RequestHeader} method parameters.
215+
* @param requestHeader the annotation declared on the parameter
216+
* @param result the validation result
217+
*/
218+
void requestHeader(RequestHeader requestHeader, ParameterValidationResult result);
219+
220+
/**
221+
* Handle results for {@code @RequestParam} method parameters.
222+
* @param requestParam the optional {@code RequestParam} annotation,
223+
* possibly {@code null} if the method parameter is declared without it.
224+
* @param result the validation result
225+
*/
226+
void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result);
227+
228+
/**
229+
* Handle results for {@code @RequestPart} method parameters.
230+
* @param requestPart the annotation declared on the parameter
231+
* @param errors the validation errors
232+
*/
233+
void requestPart(RequestPart requestPart, ParameterErrors errors);
234+
235+
/**
236+
* Handle other results that aren't any of the above.
237+
* @param result the validation result
238+
*/
239+
void other(ParameterValidationResult result);
240+
241+
}
242+
87243
}

spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.method.annotation;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.function.Predicate;
2021

2122
import jakarta.validation.Validator;
2223

@@ -54,9 +55,17 @@ public final class HandlerMethodValidator implements MethodValidator {
5455

5556
private final MethodValidationAdapter validationAdapter;
5657

58+
private final Predicate<MethodParameter> modelAttribitePredicate;
59+
60+
private final Predicate<MethodParameter> requestParamPredicate;
61+
62+
63+
private HandlerMethodValidator(MethodValidationAdapter validationAdapter,
64+
Predicate<MethodParameter> modelAttribitePredicate, Predicate<MethodParameter> requestParamPredicate) {
5765

58-
private HandlerMethodValidator(MethodValidationAdapter validationAdapter) {
5966
this.validationAdapter = validationAdapter;
67+
this.modelAttribitePredicate = modelAttribitePredicate;
68+
this.requestParamPredicate = requestParamPredicate;
6069
}
6170

6271

@@ -93,7 +102,8 @@ public void applyArgumentValidation(
93102
}
94103
}
95104

96-
throw new HandlerMethodValidationException(result);
105+
throw new HandlerMethodValidationException(
106+
result, this.modelAttribitePredicate, this.requestParamPredicate);
97107
}
98108

99109
@Override
@@ -130,21 +140,21 @@ public MethodValidationResult validateReturnValue(Object target, Method method,
130140
*/
131141
@Nullable
132142
public static MethodValidator from(
133-
@Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer) {
143+
@Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer,
144+
Predicate<MethodParameter> modelAttribitePredicate, Predicate<MethodParameter> requestParamPredicate) {
134145

135146
if (initializer instanceof ConfigurableWebBindingInitializer configurableInitializer) {
136147
if (configurableInitializer.getValidator() instanceof Validator validator) {
137148
MethodValidationAdapter adapter = new MethodValidationAdapter(validator);
149+
adapter.setObjectNameResolver(objectNameResolver);
138150
if (paramNameDiscoverer != null) {
139151
adapter.setParameterNameDiscoverer(paramNameDiscoverer);
140152
}
141153
MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver();
142154
if (codesResolver != null) {
143155
adapter.setMessageCodesResolver(codesResolver);
144156
}
145-
HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter);
146-
adapter.setObjectNameResolver(objectNameResolver);
147-
return methodValidator;
157+
return new HandlerMethodValidator(adapter, modelAttribitePredicate, requestParamPredicate);
148158
}
149159
}
150160
return null;

spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -127,7 +127,7 @@ public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewC
127127
* the given method parameter.
128128
*/
129129
@Nullable
130-
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
130+
public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
131131
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
132132
if (result == null) {
133133
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {

0 commit comments

Comments
 (0)