Skip to content

Commit

Permalink
Add Visitor to HandlerMethodValidationException
Browse files Browse the repository at this point in the history
Closes gh-30813
  • Loading branch information
rstoyanchev committed Jul 4, 2023
1 parent 2a126fa commit deaa493
Show file tree
Hide file tree
Showing 10 changed files with 497 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ default List<? extends MessageSourceResolvable> getAllErrors() {
List<ParameterValidationResult> getAllValidationResults();

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

/**
* Return only validation results for Object method parameters with nested
* errors on their fields and properties. This excludes method parameters
* with errors directly on them.
* Return the subset of {@link #getAllValidationResults() allValidationResults}
* that includes Object method parameters with nested errors on their fields
* and properties. This excludes {@link #getValueResults() valueResults} with
* validation errors directly on method arguments.
*/
default List<ParameterErrors> getBeanResults() {
return getAllValidationResults().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,24 @@
import java.lang.reflect.Method;
import java.util.List;
import java.util.Locale;
import java.util.function.Predicate;

import org.springframework.context.MessageSource;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.validation.method.MethodValidationResult;
import org.springframework.validation.method.ParameterErrors;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.util.BindErrorUtils;

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

private final MethodValidationResult validationResult;

private final Predicate<MethodParameter> modelAttribitePredicate;

private final Predicate<MethodParameter> requestParamPredicate;


public HandlerMethodValidationException(MethodValidationResult validationResult) {
this(validationResult,
param -> param.hasParameterAnnotation(ModelAttribute.class),
param -> param.hasParameterAnnotation(RequestParam.class));
}

public HandlerMethodValidationException(MethodValidationResult validationResult,
Predicate<MethodParameter> modelAttribitePredicate, Predicate<MethodParameter> requestParamPredicate) {

super(initHttpStatus(validationResult), "Validation failure", null, null, null);
this.validationResult = validationResult;
this.modelAttribitePredicate = modelAttribitePredicate;
this.requestParamPredicate = requestParamPredicate;
}

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

/**
* Provide a {@link Visitor Visitor} to handle {@link ParameterValidationResult}s
* through callback methods organized by controller method parameter type.
*/
public void visitResults(Visitor visitor) {
for (ParameterValidationResult result : getAllValidationResults()) {
MethodParameter param = result.getMethodParameter();
CookieValue cookieValue = param.getParameterAnnotation(CookieValue.class);
if (cookieValue != null) {
visitor.cookieValue(cookieValue, result);
continue;
}
MatrixVariable matrixVariable = param.getParameterAnnotation(MatrixVariable.class);
if (matrixVariable != null) {
visitor.matrixVariable(matrixVariable, result);
continue;
}
if (this.modelAttribitePredicate.test(param)) {
ModelAttribute modelAttribute = param.getParameterAnnotation(ModelAttribute.class);
visitor.modelAttribute(modelAttribute, asErrors(result));
continue;
}
PathVariable pathVariable = param.getParameterAnnotation(PathVariable.class);
if (pathVariable != null) {
visitor.pathVariable(pathVariable, result);
continue;
}
RequestBody requestBody = param.getParameterAnnotation(RequestBody.class);
if (requestBody != null) {
visitor.requestBody(requestBody, asErrors(result));
continue;
}
RequestHeader requestHeader = param.getParameterAnnotation(RequestHeader.class);
if (requestHeader != null) {
visitor.requestHeader(requestHeader, result);
continue;
}
if (this.requestParamPredicate.test(param)) {
RequestParam requestParam = param.getParameterAnnotation(RequestParam.class);
visitor.requestParam(requestParam, result);
continue;
}
RequestPart requestPart = param.getParameterAnnotation(RequestPart.class);
if (requestPart != null) {
visitor.requestPart(requestPart, asErrors(result));
continue;
}
visitor.other(result);
}
}

private static ParameterErrors asErrors(ParameterValidationResult result) {
Assert.state(result instanceof ParameterErrors, "Expected ParameterErrors");
return (ParameterErrors) result;
}


/**
* Contract to handle validation results with callbacks by controller method
* parameter type, with {@link #other} serving as the fallthrough.
*/
public interface Visitor {

/**
* Handle results for {@code @CookieValue} method parameters.
* @param cookieValue the annotation declared on the parameter
* @param result the validation result
*/
void cookieValue(CookieValue cookieValue, ParameterValidationResult result);

/**
* Handle results for {@code @MatrixVariable} method parameters.
* @param matrixVariable the annotation declared on the parameter
* @param result the validation result
*/
void matrixVariable(MatrixVariable matrixVariable, ParameterValidationResult result);

/**
* Handle results for {@code @ModelAttribute} method parameters.
* @param modelAttribute the optional {@code ModelAttribute} annotation,
* possibly {@code null} if the method parameter is declared without it.
* @param errors the validation errors
*/
void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors);

/**
* Handle results for {@code @PathVariable} method parameters.
* @param pathVariable the annotation declared on the parameter
* @param result the validation result
*/
void pathVariable(PathVariable pathVariable, ParameterValidationResult result);

/**
* Handle results for {@code @RequestBody} method parameters.
* @param requestBody the annotation declared on the parameter
* @param errors the validation error
*/
void requestBody(RequestBody requestBody, ParameterErrors errors);

/**
* Handle results for {@code @RequestHeader} method parameters.
* @param requestHeader the annotation declared on the parameter
* @param result the validation result
*/
void requestHeader(RequestHeader requestHeader, ParameterValidationResult result);

/**
* Handle results for {@code @RequestParam} method parameters.
* @param requestParam the optional {@code RequestParam} annotation,
* possibly {@code null} if the method parameter is declared without it.
* @param result the validation result
*/
void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result);

/**
* Handle results for {@code @RequestPart} method parameters.
* @param requestPart the annotation declared on the parameter
* @param errors the validation errors
*/
void requestPart(RequestPart requestPart, ParameterErrors errors);

/**
* Handle other results that aren't any of the above.
* @param result the validation result
*/
void other(ParameterValidationResult result);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.web.method.annotation;

import java.lang.reflect.Method;
import java.util.function.Predicate;

import jakarta.validation.Validator;

Expand Down Expand Up @@ -54,9 +55,17 @@ public final class HandlerMethodValidator implements MethodValidator {

private final MethodValidationAdapter validationAdapter;

private final Predicate<MethodParameter> modelAttribitePredicate;

private final Predicate<MethodParameter> requestParamPredicate;


private HandlerMethodValidator(MethodValidationAdapter validationAdapter,
Predicate<MethodParameter> modelAttribitePredicate, Predicate<MethodParameter> requestParamPredicate) {

private HandlerMethodValidator(MethodValidationAdapter validationAdapter) {
this.validationAdapter = validationAdapter;
this.modelAttribitePredicate = modelAttribitePredicate;
this.requestParamPredicate = requestParamPredicate;
}


Expand Down Expand Up @@ -93,7 +102,8 @@ public void applyArgumentValidation(
}
}

throw new HandlerMethodValidationException(result);
throw new HandlerMethodValidationException(
result, this.modelAttribitePredicate, this.requestParamPredicate);
}

@Override
Expand Down Expand Up @@ -130,21 +140,21 @@ public MethodValidationResult validateReturnValue(Object target, Method method,
*/
@Nullable
public static MethodValidator from(
@Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer) {
@Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer,
Predicate<MethodParameter> modelAttribitePredicate, Predicate<MethodParameter> requestParamPredicate) {

if (initializer instanceof ConfigurableWebBindingInitializer configurableInitializer) {
if (configurableInitializer.getValidator() instanceof Validator validator) {
MethodValidationAdapter adapter = new MethodValidationAdapter(validator);
adapter.setObjectNameResolver(objectNameResolver);
if (paramNameDiscoverer != null) {
adapter.setParameterNameDiscoverer(paramNameDiscoverer);
}
MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver();
if (codesResolver != null) {
adapter.setMessageCodesResolver(codesResolver);
}
HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter);
adapter.setObjectNameResolver(objectNameResolver);
return methodValidator;
return new HandlerMethodValidator(adapter, modelAttribitePredicate, requestParamPredicate);
}
}
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 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.
Expand Down Expand Up @@ -127,7 +127,7 @@ public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewC
* the given method parameter.
*/
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
Expand Down
Loading

0 comments on commit deaa493

Please sign in to comment.