Support cross-parameter validation
Closes gh-33271
This commit is contained in:
@@ -18,6 +18,7 @@ package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -301,6 +302,7 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
|
||||
Map<Path.Node, ParamValidationResultBuilder> paramViolations = new LinkedHashMap<>();
|
||||
Map<Path.Node, ParamErrorsBuilder> nestedViolations = new LinkedHashMap<>();
|
||||
List<MessageSourceResolvable> crossParamErrors = null;
|
||||
|
||||
for (ConstraintViolation<Object> violation : violations) {
|
||||
Iterator<Path.Node> nodes = violation.getPropertyPath().iterator();
|
||||
@@ -315,6 +317,11 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
|
||||
parameter = parameterFunction.apply(-1);
|
||||
}
|
||||
else if (node.getKind().equals(ElementKind.CROSS_PARAMETER)) {
|
||||
crossParamErrors = (crossParamErrors != null ? crossParamErrors : new ArrayList<>());
|
||||
crossParamErrors.add(createCrossParamError(target, method, violation));
|
||||
break;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
@@ -382,7 +389,8 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
nestedViolations.forEach((key, builder) -> resultList.add(builder.build()));
|
||||
resultList.sort(resultComparator);
|
||||
|
||||
return MethodValidationResult.create(target, method, resultList);
|
||||
return MethodValidationResult.create(target, method, resultList,
|
||||
(crossParamErrors != null ? crossParamErrors : Collections.emptyList()));
|
||||
}
|
||||
|
||||
private MethodParameter initMethodParameter(Method method, int index) {
|
||||
@@ -413,6 +421,19 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
return result;
|
||||
}
|
||||
|
||||
private MessageSourceResolvable createCrossParamError(
|
||||
Object target, Method method, ConstraintViolation<Object> violation) {
|
||||
|
||||
String objectName = Conventions.getVariableName(target) + "#" + method.getName();
|
||||
|
||||
ConstraintDescriptor<?> descriptor = violation.getConstraintDescriptor();
|
||||
String code = descriptor.getAnnotation().annotationType().getSimpleName();
|
||||
String[] codes = this.messageCodesResolver.resolveMessageCodes(code, objectName);
|
||||
Object[] arguments = this.validatorAdapter.get().getArgumentsForConstraint(objectName, "", descriptor);
|
||||
|
||||
return new ViolationMessageSourceResolvable(codes, arguments, violation.getMessage(), violation);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Strategy to resolve the name of an {@code @Valid} method parameter to
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.validation.method;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -33,19 +34,26 @@ final class DefaultMethodValidationResult implements MethodValidationResult {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final List<ParameterValidationResult> allValidationResults;
|
||||
private final List<ParameterValidationResult> parameterValidationResults;
|
||||
|
||||
private final List<MessageSourceResolvable> crossParamResults;
|
||||
|
||||
private final boolean forReturnValue;
|
||||
|
||||
|
||||
DefaultMethodValidationResult(Object target, Method method, List<ParameterValidationResult> results) {
|
||||
Assert.notEmpty(results, "'results' is required and must not be empty");
|
||||
DefaultMethodValidationResult(
|
||||
Object target, Method method, List<ParameterValidationResult> results,
|
||||
List<MessageSourceResolvable> crossParamResults) {
|
||||
|
||||
Assert.isTrue(!results.isEmpty() || !crossParamResults.isEmpty(), "Expected validation results");
|
||||
Assert.notNull(target, "'target' is required");
|
||||
Assert.notNull(method, "Method is required");
|
||||
|
||||
this.target = target;
|
||||
this.method = method;
|
||||
this.allValidationResults = results;
|
||||
this.forReturnValue = (results.get(0).getMethodParameter().getParameterIndex() == -1);
|
||||
this.parameterValidationResults = results;
|
||||
this.crossParamResults = crossParamResults;
|
||||
this.forReturnValue = (!results.isEmpty() && results.get(0).getMethodParameter().getParameterIndex() == -1);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,10 +73,14 @@ final class DefaultMethodValidationResult implements MethodValidationResult {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParameterValidationResult> getAllValidationResults() {
|
||||
return this.allValidationResults;
|
||||
public List<ParameterValidationResult> getParameterValidationResults() {
|
||||
return this.parameterValidationResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageSourceResolvable> getCrossParameterValidationResults() {
|
||||
return this.crossParamResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -20,6 +20,8 @@ import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
|
||||
/**
|
||||
* {@link MethodValidationResult} with an empty list of results.
|
||||
*
|
||||
@@ -44,7 +46,12 @@ final class EmptyMethodValidationResult implements MethodValidationResult {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParameterValidationResult> getAllValidationResults() {
|
||||
public List<ParameterValidationResult> getParameterValidationResults() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageSourceResolvable> getCrossParameterValidationResults() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.validation.method;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -57,8 +58,13 @@ public class MethodValidationException extends RuntimeException implements Metho
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParameterValidationResult> getAllValidationResults() {
|
||||
return this.validationResult.getAllValidationResults();
|
||||
public List<ParameterValidationResult> getParameterValidationResults() {
|
||||
return this.validationResult.getParameterValidationResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageSourceResolvable> getCrossParameterValidationResults() {
|
||||
return this.validationResult.getCrossParameterValidationResults();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.validation.method;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
@@ -55,54 +56,75 @@ public interface MethodValidationResult {
|
||||
* Whether the result contains any validation errors.
|
||||
*/
|
||||
default boolean hasErrors() {
|
||||
return !getAllValidationResults().isEmpty();
|
||||
return !getParameterValidationResults().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single list with all errors from all validation results.
|
||||
* @see #getAllValidationResults()
|
||||
* @see #getParameterValidationResults()
|
||||
* @see ParameterValidationResult#getResolvableErrors()
|
||||
*/
|
||||
default List<? extends MessageSourceResolvable> getAllErrors() {
|
||||
return getAllValidationResults().stream()
|
||||
return getParameterValidationResults().stream()
|
||||
.flatMap(result -> result.getResolvableErrors().stream())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all validation results per method parameter, including both
|
||||
* {@link #getValueResults()} and {@link #getBeanResults()}.
|
||||
* <p>Use {@link #getCrossParameterValidationResults()} for access to errors
|
||||
* from cross-parameter validation.
|
||||
* @since 6.2
|
||||
* @see #getValueResults()
|
||||
* @see #getBeanResults()
|
||||
*/
|
||||
List<ParameterValidationResult> getParameterValidationResults();
|
||||
|
||||
/**
|
||||
* Return all validation results. This includes both method parameters with
|
||||
* errors directly on them, and Object method parameters with nested errors
|
||||
* on their fields and properties.
|
||||
* @see #getValueResults()
|
||||
* @see #getBeanResults()
|
||||
* @deprecated deprecated in favor of {@link #getParameterValidationResults()}
|
||||
*/
|
||||
List<ParameterValidationResult> getAllValidationResults();
|
||||
@Deprecated(since = "6.2", forRemoval = true)
|
||||
default List<ParameterValidationResult> getAllValidationResults() {
|
||||
return getParameterValidationResults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the subset of {@link #getAllValidationResults() allValidationResults}
|
||||
* Return the subset of {@link #getParameterValidationResults() 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()
|
||||
return getParameterValidationResults().stream()
|
||||
.filter(result -> !(result instanceof ParameterErrors))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the subset of {@link #getAllValidationResults() allValidationResults}
|
||||
* Return the subset of {@link #getParameterValidationResults() 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()
|
||||
return getParameterValidationResults().stream()
|
||||
.filter(ParameterErrors.class::isInstance)
|
||||
.map(result -> (ParameterErrors) result)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return errors from cross-parameter validation.
|
||||
* @since 6.2
|
||||
*/
|
||||
List<MessageSourceResolvable> getCrossParameterValidationResults();
|
||||
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link MethodValidationResult} instance.
|
||||
@@ -112,7 +134,23 @@ public interface MethodValidationResult {
|
||||
* @return the created instance
|
||||
*/
|
||||
static MethodValidationResult create(Object target, Method method, List<ParameterValidationResult> results) {
|
||||
return new DefaultMethodValidationResult(target, method, results);
|
||||
return create(target, method, results, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link MethodValidationResult} instance.
|
||||
* @param target the target Object
|
||||
* @param method the target method
|
||||
* @param results method validation results, expected to be non-empty
|
||||
* @param crossParameterErrors cross-parameter validation errors
|
||||
* @return the created instance
|
||||
* @since 6.2
|
||||
*/
|
||||
static MethodValidationResult create(
|
||||
Object target, Method method, List<ParameterValidationResult> results,
|
||||
List<MessageSourceResolvable> crossParameterErrors) {
|
||||
|
||||
return new DefaultMethodValidationResult(target, method, results, crossParameterErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user