Adapt ConstraintViolation's from method validation
See gh-29825
This commit is contained in:
parent
38abee00e2
commit
425d5a94cb
|
@ -0,0 +1,459 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ElementKind;
|
||||
import jakarta.validation.Path;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import jakarta.validation.executable.ExecutableValidator;
|
||||
import jakarta.validation.metadata.ConstraintDescriptor;
|
||||
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.DefaultMessageCodesResolver;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.MessageCodesResolver;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* Assist with applying method-level validation via
|
||||
* {@link jakarta.validation.Validator}, adapt each resulting
|
||||
* {@link ConstraintViolation} to {@link ParameterValidationResult}, and
|
||||
* raise {@link MethodValidationException}.
|
||||
*
|
||||
* <p>Used by {@link MethodValidationInterceptor}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.1
|
||||
*/
|
||||
public class MethodValidationAdapter {
|
||||
|
||||
private static final Comparator<ParameterValidationResult> RESULT_COMPARATOR = new ResultComparator();
|
||||
|
||||
|
||||
private final Supplier<Validator> validator;
|
||||
|
||||
private final Supplier<SpringValidatorAdapter> validatorAdapter;
|
||||
|
||||
private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
|
||||
|
||||
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance using a default JSR-303 validator underneath.
|
||||
*/
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public MethodValidationAdapter() {
|
||||
this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator());
|
||||
this.validatorAdapter = SingletonSupplier.of(() -> new SpringValidatorAdapter(this.validator.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance using the given JSR-303 ValidatorFactory.
|
||||
* @param validatorFactory the JSR-303 ValidatorFactory to use
|
||||
*/
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public MethodValidationAdapter(ValidatorFactory validatorFactory) {
|
||||
this.validator = SingletonSupplier.of(validatorFactory::getValidator);
|
||||
this.validatorAdapter = SingletonSupplier.of(() -> new SpringValidatorAdapter(this.validator.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance using the given JSR-303 Validator.
|
||||
* @param validator the JSR-303 Validator to use
|
||||
*/
|
||||
public MethodValidationAdapter(Validator validator) {
|
||||
this.validator = () -> validator;
|
||||
this.validatorAdapter = () -> new SpringValidatorAdapter(validator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance for the supplied (potentially lazily initialized) Validator.
|
||||
* @param validator a Supplier for the Validator to use
|
||||
*/
|
||||
public MethodValidationAdapter(Supplier<Validator> validator) {
|
||||
this.validator = validator;
|
||||
this.validatorAdapter = () -> new SpringValidatorAdapter(this.validator.get());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the strategy to use to determine message codes for violations.
|
||||
* <p>Default is a DefaultMessageCodesResolver.
|
||||
*/
|
||||
public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) {
|
||||
this.messageCodesResolver = messageCodesResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link #setMessageCodesResolver(MessageCodesResolver) configured}
|
||||
* {@code MessageCodesResolver}.
|
||||
*/
|
||||
public MessageCodesResolver getMessageCodesResolver() {
|
||||
return this.messageCodesResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ParameterNameDiscoverer to use to resolve method parameter names
|
||||
* that is in turn used to create error codes for {@link MessageSourceResolvable}.
|
||||
* <p>Default is {@link org.springframework.core.DefaultParameterNameDiscoverer}.
|
||||
*/
|
||||
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
this.parameterNameDiscoverer = parameterNameDiscoverer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link #setParameterNameDiscoverer(ParameterNameDiscoverer) configured}
|
||||
* {@code ParameterNameDiscoverer}.
|
||||
*/
|
||||
public ParameterNameDiscoverer getParameterNameDiscoverer() {
|
||||
return this.parameterNameDiscoverer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use this method determine the validation groups to pass into
|
||||
* {@link #validateMethodArguments(Object, Method, Object[], Class[])} and
|
||||
* {@link #validateMethodReturnValue(Object, Method, Object, Class[])}.
|
||||
* <p>Default are the validation groups as specified in the {@link Validated}
|
||||
* annotation on the method, or on the containing target class of the method,
|
||||
* or for an AOP proxy without a target (with all behavior in advisors), also
|
||||
* check on proxied interfaces.
|
||||
* @param target the target Object
|
||||
* @param method the target method
|
||||
* @return the applicable validation groups as a {@code Class} array
|
||||
*/
|
||||
public static Class<?>[] determineValidationGroups(Object target, Method method) {
|
||||
Validated validatedAnn = AnnotationUtils.findAnnotation(method, Validated.class);
|
||||
if (validatedAnn == null) {
|
||||
if (AopUtils.isAopProxy(target)) {
|
||||
for (Class<?> type : AopProxyUtils.proxiedUserInterfaces(target)) {
|
||||
validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class);
|
||||
if (validatedAnn != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
|
||||
}
|
||||
}
|
||||
return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given method arguments and raise {@link ConstraintViolation}
|
||||
* in case of any errors.
|
||||
* @param target the target Object
|
||||
* @param method the target method
|
||||
* @param arguments candidate arguments for a method invocation
|
||||
* @param groups groups for validation determined via
|
||||
* {@link #determineValidationGroups(Object, Method)}
|
||||
*/
|
||||
public void validateMethodArguments(Object target, Method method, Object[] arguments, Class<?>[] groups) {
|
||||
ExecutableValidator execVal = this.validator.get().forExecutables();
|
||||
Set<ConstraintViolation<Object>> result;
|
||||
try {
|
||||
result = execVal.validateParameters(target, method, arguments, groups);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
|
||||
// Let's try to find the bridged method on the implementation class...
|
||||
Method mostSpecificMethod = ClassUtils.getMostSpecificMethod(method, target.getClass());
|
||||
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(mostSpecificMethod);
|
||||
result = execVal.validateParameters(target, bridgedMethod, arguments, groups);
|
||||
}
|
||||
if (!result.isEmpty()) {
|
||||
throw createException(target, method, result, i -> arguments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given return value and raise {@link ConstraintViolation}
|
||||
* in case of any errors.
|
||||
* @param target the target Object
|
||||
* @param method the target method
|
||||
* @param returnValue value returned from invoking the target method
|
||||
* @param groups groups for validation determined via
|
||||
* {@link #determineValidationGroups(Object, Method)}
|
||||
*/
|
||||
public void validateMethodReturnValue(
|
||||
Object target, Method method, @Nullable Object returnValue, Class<?>[] groups) {
|
||||
|
||||
ExecutableValidator execVal = this.validator.get().forExecutables();
|
||||
Set<ConstraintViolation<Object>> result = execVal.validateReturnValue(target, method, returnValue, groups);
|
||||
if (!result.isEmpty()) {
|
||||
throw createException(target, method, result, i -> returnValue);
|
||||
}
|
||||
}
|
||||
|
||||
private MethodValidationException createException(
|
||||
Object target, Method method, Set<ConstraintViolation<Object>> violations,
|
||||
Function<Integer, Object> argumentFunction) {
|
||||
|
||||
Map<MethodParameter, ValueResultBuilder> parameterViolations = new LinkedHashMap<>();
|
||||
Map<Path.Node, BeanResultBuilder> cascadedViolations = new LinkedHashMap<>();
|
||||
|
||||
for (ConstraintViolation<Object> violation : violations) {
|
||||
Iterator<Path.Node> itr = violation.getPropertyPath().iterator();
|
||||
while (itr.hasNext()) {
|
||||
Path.Node node = itr.next();
|
||||
|
||||
MethodParameter parameter;
|
||||
if (node.getKind().equals(ElementKind.PARAMETER)) {
|
||||
parameter = new MethodParameter(method, node.as(Path.ParameterNode.class).getParameterIndex());
|
||||
}
|
||||
else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
|
||||
parameter = new MethodParameter(method, -1);
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
||||
|
||||
Object argument = argumentFunction.apply(parameter.getParameterIndex());
|
||||
if (!itr.hasNext()) {
|
||||
parameterViolations
|
||||
.computeIfAbsent(parameter, p -> new ValueResultBuilder(target, parameter, argument))
|
||||
.addViolation(violation);
|
||||
}
|
||||
else {
|
||||
cascadedViolations
|
||||
.computeIfAbsent(node, n -> new BeanResultBuilder(parameter, argument, itr.next()))
|
||||
.addViolation(violation);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<ParameterValidationResult> validatonResultList = new ArrayList<>();
|
||||
parameterViolations.forEach((parameter, builder) -> validatonResultList.add(builder.build()));
|
||||
cascadedViolations.forEach((node, builder) -> validatonResultList.add(builder.build()));
|
||||
validatonResultList.sort(RESULT_COMPARATOR);
|
||||
|
||||
return new MethodValidationException(target, method, validatonResultList, violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link MessageSourceResolvable} for the given violation.
|
||||
* @param target target of the method invocation to which validation was applied
|
||||
* @param parameter the method parameter associated with the violation
|
||||
* @param violation the violation
|
||||
* @return the created {@code MessageSourceResolvable}
|
||||
*/
|
||||
private MessageSourceResolvable createMessageSourceResolvable(
|
||||
Object target, MethodParameter parameter, ConstraintViolation<Object> violation) {
|
||||
|
||||
String objectName = Conventions.getVariableName(target) + "#" + parameter.getExecutable().getName();
|
||||
String paramName = (parameter.getParameterName() != null ? parameter.getParameterName() : "");
|
||||
Class<?> parameterType = parameter.getParameterType();
|
||||
|
||||
ConstraintDescriptor<?> descriptor = violation.getConstraintDescriptor();
|
||||
String code = descriptor.getAnnotation().annotationType().getSimpleName();
|
||||
String[] codes = this.messageCodesResolver.resolveMessageCodes(code, objectName, paramName, parameterType);
|
||||
Object[] arguments = this.validatorAdapter.get().getArgumentsForConstraint(objectName, paramName, descriptor);
|
||||
|
||||
return new DefaultMessageSourceResolvable(codes, arguments, violation.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an object name and create a {@link BindingResult} for the argument.
|
||||
* <p>By default, the name is based on the parameter name, or for a return type on
|
||||
* {@link Conventions#getVariableNameForReturnType(Method, Class, Object)}.
|
||||
* <p>If a name cannot be determined for any reason, e.g. a return value with
|
||||
* insufficient type information, then {@code "{methodName}.arg{index}"} is used.
|
||||
* @param parameter the method parameter
|
||||
* @param argument the argument value
|
||||
* @return the determined name
|
||||
*/
|
||||
private BindingResult createBindingResult(MethodParameter parameter, @Nullable Object argument) {
|
||||
// TODO: allow external customization via Function (e.g. from @ModelAttribute + Conventions based on type)
|
||||
String objectName = parameter.getParameterName();
|
||||
int index = parameter.getParameterIndex();
|
||||
if (index == -1) {
|
||||
try {
|
||||
Method method = parameter.getMethod();
|
||||
if (method != null) {
|
||||
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, parameter.getContainingClass());
|
||||
objectName = Conventions.getVariableNameForReturnType(method, resolvedType, argument);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// insufficient type information
|
||||
}
|
||||
}
|
||||
if (objectName == null) {
|
||||
objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ""));
|
||||
}
|
||||
BeanPropertyBindingResult result = new BeanPropertyBindingResult(argument, objectName);
|
||||
result.setMessageCodesResolver(this.messageCodesResolver);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a validation result for a value method parameter with constraints
|
||||
* declared directly on it.
|
||||
*/
|
||||
private final class ValueResultBuilder {
|
||||
|
||||
private final Object target;
|
||||
|
||||
private final MethodParameter parameter;
|
||||
|
||||
@Nullable
|
||||
private final Object argument;
|
||||
|
||||
private final List<MessageSourceResolvable> resolvableErrors = new ArrayList<>();
|
||||
|
||||
private final Set<ConstraintViolation<Object>> violations = new LinkedHashSet<>();
|
||||
|
||||
public ValueResultBuilder(Object target, MethodParameter parameter, @Nullable Object argument) {
|
||||
this.target = target;
|
||||
this.parameter = parameter;
|
||||
this.argument = argument;
|
||||
}
|
||||
|
||||
public void addViolation(ConstraintViolation<Object> violation) {
|
||||
this.violations.add(violation);
|
||||
this.resolvableErrors.add(createMessageSourceResolvable(this.target, this.parameter, violation));
|
||||
}
|
||||
|
||||
public ParameterValidationResult build() {
|
||||
return new ParameterValidationResult(
|
||||
this.parameter, this.argument, this.resolvableErrors, this.violations);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a validation result for an {@link jakarta.validation.Valid @Valid}
|
||||
* annotated bean method parameter with cascaded constraints.
|
||||
*/
|
||||
private final class BeanResultBuilder {
|
||||
|
||||
private final MethodParameter parameter;
|
||||
|
||||
@Nullable
|
||||
private final Object argument;
|
||||
|
||||
@Nullable
|
||||
private final Object container;
|
||||
|
||||
@Nullable
|
||||
private final Integer containerIndex;
|
||||
|
||||
@Nullable
|
||||
private final Object containerKey;
|
||||
|
||||
private final Errors errors;
|
||||
|
||||
private final Set<ConstraintViolation<Object>> violations = new LinkedHashSet<>();
|
||||
|
||||
public BeanResultBuilder(MethodParameter parameter, @Nullable Object argument, Path.Node node) {
|
||||
this.parameter = parameter;
|
||||
|
||||
this.containerIndex = node.getIndex();
|
||||
this.containerKey = node.getKey();
|
||||
if (argument instanceof List<?> list && this.containerIndex != null) {
|
||||
this.container = list;
|
||||
argument = list.get(this.containerIndex);
|
||||
}
|
||||
else if (argument instanceof Map<?, ?> map && this.containerKey != null) {
|
||||
this.container = map;
|
||||
argument = map.get(this.containerKey);
|
||||
}
|
||||
else {
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
this.argument = argument;
|
||||
this.errors = createBindingResult(parameter, argument);
|
||||
}
|
||||
|
||||
public void addViolation(ConstraintViolation<Object> violation) {
|
||||
this.violations.add(violation);
|
||||
}
|
||||
|
||||
public ParameterErrors build() {
|
||||
validatorAdapter.get().processConstraintViolations(this.violations, this.errors);
|
||||
return new ParameterErrors(
|
||||
this.parameter, this.argument, this.errors, this.violations,
|
||||
this.container, this.containerIndex, this.containerKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Comparator for validation results, sorted by method parameter index first,
|
||||
* also falling back on container indexes if necessary for cascaded
|
||||
* constraints on a List container.
|
||||
*/
|
||||
private final static class ResultComparator implements Comparator<ParameterValidationResult> {
|
||||
|
||||
@Override
|
||||
public int compare(ParameterValidationResult result1, ParameterValidationResult result2) {
|
||||
int index1 = result1.getMethodParameter().getParameterIndex();
|
||||
int index2 = result2.getMethodParameter().getParameterIndex();
|
||||
int i = Integer.compare(index1, index2);
|
||||
if (i != 0) {
|
||||
return i;
|
||||
}
|
||||
if (result1 instanceof ParameterErrors errors1 && result2 instanceof ParameterErrors errors2) {
|
||||
Integer containerIndex1 = errors1.getContainerIndex();
|
||||
Integer containerIndex2 = errors2.getContainerIndex();
|
||||
if (containerIndex1 != null && containerIndex2 != null) {
|
||||
i = Integer.compare(containerIndex1, containerIndex2);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import jakarta.validation.executable.ExecutableValidator;
|
||||
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* Helper class to apply method-level validation on annotated methods via
|
||||
* {@link jakarta.validation.Valid}.
|
||||
*
|
||||
* <p>Used by {@link MethodValidationInterceptor}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.1.0
|
||||
*/
|
||||
public class MethodValidationDelegate {
|
||||
|
||||
private final Supplier<Validator> validator;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance using a default JSR-303 validator underneath.
|
||||
*/
|
||||
public MethodValidationDelegate() {
|
||||
this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance using the given JSR-303 ValidatorFactory.
|
||||
* @param validatorFactory the JSR-303 ValidatorFactory to use
|
||||
*/
|
||||
public MethodValidationDelegate(ValidatorFactory validatorFactory) {
|
||||
this.validator = SingletonSupplier.of(validatorFactory::getValidator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance using the given JSR-303 Validator.
|
||||
* @param validator the JSR-303 Validator to use
|
||||
*/
|
||||
public MethodValidationDelegate(Validator validator) {
|
||||
this.validator = () -> validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance for the supplied (potentially lazily initialized) Validator.
|
||||
* @param validator a Supplier for the Validator to use
|
||||
*/
|
||||
public MethodValidationDelegate(Supplier<Validator> validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use this method determine the validation groups to pass into
|
||||
* {@link #validateMethodArguments(Object, Method, Object[], Class[])} and
|
||||
* {@link #validateMethodReturnValue(Object, Method, Object, Class[])}.
|
||||
* <p>Default are the validation groups as specified in the {@link Validated}
|
||||
* annotation on the method, or on the containing target class of the method,
|
||||
* or for an AOP proxy without a target (with all behavior in advisors), also
|
||||
* check on proxied interfaces.
|
||||
* @param target the target Object
|
||||
* @param method the target method
|
||||
* @return the applicable validation groups as a {@code Class} array
|
||||
*/
|
||||
public Class<?>[] determineValidationGroups(Object target, Method method) {
|
||||
Validated validatedAnn = AnnotationUtils.findAnnotation(method, Validated.class);
|
||||
if (validatedAnn == null) {
|
||||
if (AopUtils.isAopProxy(target)) {
|
||||
for (Class<?> type : AopProxyUtils.proxiedUserInterfaces(target)) {
|
||||
validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class);
|
||||
if (validatedAnn != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
|
||||
}
|
||||
}
|
||||
return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given method arguments and raise {@link ConstraintViolation}
|
||||
* in case of any errors.
|
||||
* @param target the target Object
|
||||
* @param method the target method
|
||||
* @param arguments candidate arguments for a method invocation
|
||||
* @param groups groups for validation determined via
|
||||
* {@link #determineValidationGroups(Object, Method)}
|
||||
*/
|
||||
public void validateMethodArguments(Object target, Method method, Object[] arguments, Class<?>[] groups) {
|
||||
ExecutableValidator execVal = this.validator.get().forExecutables();
|
||||
Set<ConstraintViolation<Object>> result;
|
||||
try {
|
||||
result = execVal.validateParameters(target, method, arguments, groups);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
|
||||
// Let's try to find the bridged method on the implementation class...
|
||||
Method mostSpecificMethod = ClassUtils.getMostSpecificMethod(method, target.getClass());
|
||||
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(mostSpecificMethod);
|
||||
result = execVal.validateParameters(target, bridgedMethod, arguments, groups);
|
||||
}
|
||||
if (!result.isEmpty()) {
|
||||
throw new ConstraintViolationException(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given return value and raise {@link ConstraintViolation}
|
||||
* in case of any errors.
|
||||
* @param target the target Object
|
||||
* @param method the target method
|
||||
* @param returnValue value returned from invoking the target method
|
||||
* @param groups groups for validation determined via
|
||||
* {@link #determineValidationGroups(Object, Method)}
|
||||
*/
|
||||
public void validateMethodReturnValue(
|
||||
Object target, Method method, @Nullable Object returnValue, Class<?>[] groups) {
|
||||
|
||||
ExecutableValidator execVal = this.validator.get().forExecutables();
|
||||
Set<ConstraintViolation<Object>> result = execVal.validateReturnValue(target, method, returnValue, groups);
|
||||
if (!result.isEmpty()) {
|
||||
throw new ConstraintViolationException(result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
|
||||
/**
|
||||
* Extension of {@link ConstraintViolationException} that exposes an additional
|
||||
* list of {@link ParameterValidationResult} with violations adapted to
|
||||
* {@link org.springframework.context.MessageSourceResolvable} and grouped by
|
||||
* method parameter.
|
||||
*
|
||||
* <p>For {@link jakarta.validation.Valid @Valid}-annotated, Object method
|
||||
* parameters or return types with cascaded violations, the {@link ParameterErrors}
|
||||
* subclass of {@link ParameterValidationResult} implements
|
||||
* {@link org.springframework.validation.Errors} and exposes
|
||||
* {@link org.springframework.validation.FieldError field errors}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.1
|
||||
* @see ParameterValidationResult
|
||||
* @see ParameterErrors
|
||||
* @see MethodValidationAdapter
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MethodValidationException extends ConstraintViolationException {
|
||||
|
||||
private final Object target;
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final List<ParameterValidationResult> allValidationResults;
|
||||
|
||||
|
||||
public MethodValidationException(
|
||||
Object target, Method method,
|
||||
List<ParameterValidationResult> validationResults,
|
||||
Set<? extends ConstraintViolation<?>> violations) {
|
||||
|
||||
super(violations);
|
||||
this.target = target;
|
||||
this.method = method;
|
||||
this.allValidationResults = validationResults;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the target of the method invocation to which validation was applied.
|
||||
*/
|
||||
public Object getTarget() {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method to which validation was applied.
|
||||
*/
|
||||
public Method getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all validation results. This includes method parameters with
|
||||
* constraints declared on them, as well as
|
||||
* {@link jakarta.validation.Valid @Valid} method parameters with
|
||||
* cascaded constraints.
|
||||
* @see #getValueResults()
|
||||
* @see #getBeanResults()
|
||||
*/
|
||||
public List<ParameterValidationResult> getAllValidationResults() {
|
||||
return this.allValidationResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only validation results for method parameters with constraints
|
||||
* declared directly on them. This excludes
|
||||
* {@link jakarta.validation.Valid @Valid} method parameters with cascaded
|
||||
* constraints.
|
||||
* @see #getAllValidationResults()
|
||||
*/
|
||||
public List<ParameterValidationResult> getValueResults() {
|
||||
return this.allValidationResults.stream()
|
||||
.filter(result -> !(result instanceof ParameterErrors))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only validation results for {@link jakarta.validation.Valid @Valid}
|
||||
* method parameters with cascaded constraints. This excludes method
|
||||
* parameters with constraints declared directly on them.
|
||||
* @see #getAllValidationResults()
|
||||
*/
|
||||
public List<ParameterErrors> getBeanResults() {
|
||||
return this.allValidationResults.stream()
|
||||
.filter(result -> result instanceof ParameterErrors)
|
||||
.map(result -> (ParameterErrors) result)
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
|
@ -55,14 +55,14 @@ import org.springframework.validation.annotation.Validated;
|
|||
*/
|
||||
public class MethodValidationInterceptor implements MethodInterceptor {
|
||||
|
||||
private final MethodValidationDelegate delegate;
|
||||
private final MethodValidationAdapter delegate;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
|
||||
*/
|
||||
public MethodValidationInterceptor() {
|
||||
this.delegate = new MethodValidationDelegate();
|
||||
this.delegate = new MethodValidationAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,7 +70,7 @@ public class MethodValidationInterceptor implements MethodInterceptor {
|
|||
* @param validatorFactory the JSR-303 ValidatorFactory to use
|
||||
*/
|
||||
public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
|
||||
this.delegate = new MethodValidationDelegate(validatorFactory);
|
||||
this.delegate = new MethodValidationAdapter(validatorFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,7 +78,7 @@ public class MethodValidationInterceptor implements MethodInterceptor {
|
|||
* @param validator the JSR-303 Validator to use
|
||||
*/
|
||||
public MethodValidationInterceptor(Validator validator) {
|
||||
this.delegate = new MethodValidationDelegate(validator);
|
||||
this.delegate = new MethodValidationAdapter(validator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,7 +88,7 @@ public class MethodValidationInterceptor implements MethodInterceptor {
|
|||
* @since 6.0
|
||||
*/
|
||||
public MethodValidationInterceptor(Supplier<Validator> validator) {
|
||||
this.delegate = new MethodValidationDelegate(validator);
|
||||
this.delegate = new MethodValidationAdapter(validator);
|
||||
}
|
||||
|
||||
|
||||
|
@ -153,7 +153,7 @@ public class MethodValidationInterceptor implements MethodInterceptor {
|
|||
protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
|
||||
Object target = getTarget(invocation);
|
||||
Method method = invocation.getMethod();
|
||||
return this.delegate.determineValidationGroups(target, method);
|
||||
return MethodValidationAdapter.determineValidationGroups(target, method);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.ObjectError;
|
||||
|
||||
/**
|
||||
* Extension of {@link ParameterValidationResult} that's created for Object
|
||||
* method arguments or return values with cascaded violations on their properties.
|
||||
* Such method parameters are annotated with {@link jakarta.validation.Valid @Valid},
|
||||
* or in the case of return values, the annotation is on the method.
|
||||
*
|
||||
* <p>In addition to the (generic) {@link #getResolvableErrors()
|
||||
* MessageSourceResolvable errors} from the base class, this subclass implements
|
||||
* {@link Errors} to expose convenient access to the same as {@link FieldError}s.
|
||||
*
|
||||
* <p>When {@code @Valid} is declared on a {@link List} or {@link java.util.Map}
|
||||
* parameter, a separate {@link ParameterErrors} is created for each list or map
|
||||
* value for which there are constraint violations. In such cases, the
|
||||
* {@link #getContainer()} is the list or map, while {@link #getContainerIndex()}
|
||||
* and {@link #getContainerKey()} reflect the index or key of the value.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.1
|
||||
*/
|
||||
public class ParameterErrors extends ParameterValidationResult implements Errors {
|
||||
|
||||
private final Errors errors;
|
||||
|
||||
@Nullable
|
||||
private final Object container;
|
||||
|
||||
@Nullable
|
||||
private final Integer containerIndex;
|
||||
|
||||
@Nullable
|
||||
private final Object containerKey;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@code ParameterErrors}.
|
||||
*/
|
||||
public ParameterErrors(
|
||||
MethodParameter parameter, @Nullable Object argument, Errors errors,
|
||||
Set<ConstraintViolation<Object>> violations,
|
||||
@Nullable Object container, @Nullable Integer index, @Nullable Object key) {
|
||||
|
||||
super(parameter, argument, new ArrayList<>(errors.getAllErrors()), violations);
|
||||
|
||||
this.errors = errors;
|
||||
this.container = container;
|
||||
this.containerIndex = index;
|
||||
this.containerKey = key;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* When {@code @Valid} is declared on a {@link List} or {@link java.util.Map}
|
||||
* method parameter, this method returns the list or map that contained the
|
||||
* validated object {@link #getArgument() argument}, while
|
||||
* {@link #getContainerIndex()} and {@link #getContainerKey()} returns the
|
||||
* respective index or key.
|
||||
*/
|
||||
@Nullable
|
||||
public Object getContainer() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@code @Valid} is declared on a {@link List}, this method returns
|
||||
* the index under which the validated object {@link #getArgument() argument}
|
||||
* is stored in the list {@link #getContainer() container}.
|
||||
*/
|
||||
@Nullable
|
||||
public Integer getContainerIndex() {
|
||||
return this.containerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@code @Valid} is declared on a {@link java.util.Map}, this method
|
||||
* returns the key under which the validated object {@link #getArgument()
|
||||
* argument} is stored in the map {@link #getContainer()}.
|
||||
*/
|
||||
@Nullable
|
||||
public Object getContainerKey() {
|
||||
return this.containerKey;
|
||||
}
|
||||
|
||||
|
||||
// Errors implementation
|
||||
|
||||
@Override
|
||||
public String getObjectName() {
|
||||
return this.errors.getObjectName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNestedPath(String nestedPath) {
|
||||
this.errors.setNestedPath(nestedPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNestedPath() {
|
||||
return this.errors.getNestedPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pushNestedPath(String subPath) {
|
||||
this.errors.pushNestedPath(subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popNestedPath() throws IllegalStateException {
|
||||
this.errors.popNestedPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String errorCode) {
|
||||
this.errors.reject(errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String errorCode, String defaultMessage) {
|
||||
this.errors.reject(errorCode, defaultMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) {
|
||||
this.errors.reject(errorCode, errorArgs, defaultMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectValue(@Nullable String field, String errorCode) {
|
||||
this.errors.rejectValue(field, errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectValue(@Nullable String field, String errorCode, String defaultMessage) {
|
||||
this.errors.rejectValue(field, errorCode, defaultMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectValue(@Nullable String field, String errorCode,
|
||||
@Nullable Object[] errorArgs, @Nullable String defaultMessage) {
|
||||
|
||||
this.errors.rejectValue(field, errorCode, errorArgs, defaultMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAllErrors(Errors errors) {
|
||||
this.errors.addAllErrors(errors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasErrors() {
|
||||
return this.errors.hasErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getErrorCount() {
|
||||
return this.errors.getErrorCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ObjectError> getAllErrors() {
|
||||
return this.errors.getAllErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasGlobalErrors() {
|
||||
return this.errors.hasGlobalErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGlobalErrorCount() {
|
||||
return this.errors.getGlobalErrorCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ObjectError> getGlobalErrors() {
|
||||
return this.errors.getGlobalErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectError getGlobalError() {
|
||||
return this.errors.getGlobalError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFieldErrors() {
|
||||
return this.errors.hasFieldErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFieldErrorCount() {
|
||||
return this.errors.getFieldErrorCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FieldError> getFieldErrors() {
|
||||
return this.errors.getFieldErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldError getFieldError() {
|
||||
return this.errors.getFieldError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFieldErrors(String field) {
|
||||
return this.errors.hasFieldErrors(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFieldErrorCount(String field) {
|
||||
return this.errors.getFieldErrorCount(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FieldError> getFieldErrors(String field) {
|
||||
return this.errors.getFieldErrors(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldError getFieldError(String field) {
|
||||
return this.errors.getFieldError(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getFieldValue(String field) {
|
||||
return this.errors.getFieldError(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getFieldType(String field) {
|
||||
return this.errors.getFieldType(field);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Store and expose the results of method validation via
|
||||
* {@link jakarta.validation.Validator} for a specific method parameter.
|
||||
* <ul>
|
||||
* <li>For a constraints directly on a method parameter, each
|
||||
* {@link ConstraintViolation} is adapted to {@link MessageSourceResolvable}.
|
||||
* <li>For cascaded constraints via {@link jakarta.validation.Validator @Valid}
|
||||
* on a bean method parameter, {@link SpringValidatorAdapter} is used to initialize
|
||||
* an {@link org.springframework.validation.Errors} with field errors, and create
|
||||
* the {@link ParameterErrors} sub-class.
|
||||
* </ul>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.1
|
||||
*/
|
||||
public class ParameterValidationResult {
|
||||
|
||||
private final MethodParameter methodParameter;
|
||||
|
||||
@Nullable
|
||||
private final Object argument;
|
||||
|
||||
private final List<MessageSourceResolvable> resolvableErrors;
|
||||
|
||||
private final Set<ConstraintViolation<Object>> violations;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@code ParameterValidationResult}.
|
||||
*/
|
||||
public ParameterValidationResult(
|
||||
MethodParameter methodParameter, @Nullable Object argument, List<MessageSourceResolvable> errors,
|
||||
Set<ConstraintViolation<Object>> violations) {
|
||||
|
||||
Assert.notNull(methodParameter, "`MethodParameter` is required");
|
||||
Assert.notEmpty(errors, "`resolvableErrors` must not be empty");
|
||||
Assert.notEmpty(violations, "'violations' must not be empty");
|
||||
this.methodParameter = methodParameter;
|
||||
this.argument = argument;
|
||||
this.resolvableErrors = List.copyOf(errors);
|
||||
this.violations = violations;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The method parameter the validation results are for.
|
||||
*/
|
||||
public MethodParameter getMethodParameter() {
|
||||
return this.methodParameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* The method argument value that was validated.
|
||||
*/
|
||||
@Nullable
|
||||
public Object getArgument() {
|
||||
return this.argument;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of {@link MessageSourceResolvable} representations adapted from the
|
||||
* underlying {@link #getViolations() violations}.
|
||||
* <ul>
|
||||
* <li>For a constraints directly on a method parameter, error codes are
|
||||
* based on the names of the constraint annotation, the object, the method,
|
||||
* the parameter, and parameter type, e.g.
|
||||
* {@code ["Max.myObject#myMethod.myParameter", "Max.myParameter", "Max.int", "Max"]}.
|
||||
* Arguments include the parameter itself as a {@link MessageSourceResolvable}, e.g.
|
||||
* {@code ["myObject#myMethod.myParameter", "myParameter"]}, followed by actual
|
||||
* constraint annotation attributes (i.e. excluding "message", "groups" and
|
||||
* "payload") in alphabetical order of attribute names.
|
||||
* <li>For cascaded constraints via {@link jakarta.validation.Validator @Valid}
|
||||
* on a bean method parameter, this method returns
|
||||
* {@link org.springframework.validation.FieldError field errors} that you
|
||||
* can also access more conveniently through methods of the
|
||||
* {@link ParameterErrors} sub-class.
|
||||
* </ul>
|
||||
*/
|
||||
public List<MessageSourceResolvable> getResolvableErrors() {
|
||||
return this.resolvableErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* The violations associated with the method parameter.
|
||||
*/
|
||||
public Set<ConstraintViolation<Object>> getViolations() {
|
||||
return this.violations;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(other)) {
|
||||
return false;
|
||||
}
|
||||
ParameterValidationResult otherResult = (ParameterValidationResult) other;
|
||||
return (getMethodParameter().equals(otherResult.getMethodParameter()) &&
|
||||
ObjectUtils.nullSafeEquals(getArgument(), otherResult.getArgument()) &&
|
||||
getViolations().equals(otherResult.getViolations()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = super.hashCode();
|
||||
hashCode = 29 * hashCode + getMethodParameter().hashCode();
|
||||
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArgument());
|
||||
hashCode = 29 * hashCode + (getViolations().hashCode());
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Validation results for method parameter '" + this.methodParameter +
|
||||
"': argument [" + ObjectUtils.nullSafeConciseToString(this.argument) + "]; " +
|
||||
getResolvableErrors();
|
||||
}
|
||||
|
||||
}
|
|
@ -38,6 +38,7 @@ import org.springframework.context.support.DefaultMessageSourceResolvable;
|
|||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.FieldError;
|
||||
|
@ -201,7 +202,7 @@ public class SpringValidatorAdapter implements SmartValidator, jakarta.validatio
|
|||
StringBuilder sb = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (Path.Node node : path) {
|
||||
if (node.isInIterable()) {
|
||||
if (node.isInIterable() && !first) {
|
||||
sb.append('[');
|
||||
Object index = node.getIndex();
|
||||
if (index == null) {
|
||||
|
@ -286,7 +287,9 @@ public class SpringValidatorAdapter implements SmartValidator, jakarta.validatio
|
|||
* @see #getArgumentsForConstraint
|
||||
*/
|
||||
protected MessageSourceResolvable getResolvableField(String objectName, String field) {
|
||||
String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field};
|
||||
String[] codes = (StringUtils.hasText(field) ?
|
||||
new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field} :
|
||||
new String[] {objectName});
|
||||
return new DefaultMessageSourceResolvable(codes, field);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.FieldError;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MethodValidationAdapter}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class MethodValidationAdapterTests {
|
||||
|
||||
private static final Person faustino1234 = new Person("Faustino1234");
|
||||
|
||||
private static final Person cayetana6789 = new Person("Cayetana6789");
|
||||
|
||||
|
||||
@Test
|
||||
void validateArguments() {
|
||||
MyService target = new MyService();
|
||||
Method method = getMethod(target, "addStudent");
|
||||
|
||||
validateArguments(target, method, new Object[] {faustino1234, cayetana6789, 3}, ex -> {
|
||||
|
||||
assertThat(ex.getConstraintViolations()).hasSize(3);
|
||||
assertThat(ex.getAllValidationResults()).hasSize(3);
|
||||
|
||||
assertBeanResult(ex.getBeanResults().get(0), 0, "student", faustino1234, List.of(
|
||||
"""
|
||||
Field error in object 'student' on field 'name': rejected value [Faustino1234]; \
|
||||
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [student.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]"""));
|
||||
|
||||
assertBeanResult(ex.getBeanResults().get(1), 1, "guardian", cayetana6789, List.of(
|
||||
"""
|
||||
Field error in object 'guardian' on field 'name': rejected value [Cayetana6789]; \
|
||||
codes [Size.guardian.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [guardian.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]"""));
|
||||
|
||||
assertValueResult(ex.getValueResults().get(0), 2, 3, List.of(
|
||||
"""
|
||||
org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [Max.myService#addStudent.degrees,Max.degrees,Max.int,Max]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [myService#addStudent.degrees,degrees]; arguments []; default message [degrees],2]; \
|
||||
default message [must be less than or equal to 2]"""
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateReturnValue() {
|
||||
MyService target = new MyService();
|
||||
|
||||
validateReturnValue(target, getMethod(target, "getIntValue"), 4, ex -> {
|
||||
|
||||
assertThat(ex.getConstraintViolations()).hasSize(1);
|
||||
assertThat(ex.getAllValidationResults()).hasSize(1);
|
||||
|
||||
assertValueResult(ex.getValueResults().get(0), -1, 4, List.of(
|
||||
"""
|
||||
org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [Min.myService#getIntValue,Min,Min.int]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [myService#getIntValue]; arguments []; default message [],5]; \
|
||||
default message [must be greater than or equal to 5]"""
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateReturnValueBean() {
|
||||
MyService target = new MyService();
|
||||
|
||||
validateReturnValue(target, getMethod(target, "getPerson"), faustino1234, ex -> {
|
||||
|
||||
assertThat(ex.getConstraintViolations()).hasSize(1);
|
||||
assertThat(ex.getAllValidationResults()).hasSize(1);
|
||||
|
||||
assertBeanResult(ex.getBeanResults().get(0), -1, "person", faustino1234, List.of(
|
||||
"""
|
||||
Field error in object 'person' on field 'name': rejected value [Faustino1234]; \
|
||||
codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [person.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]"""));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateListArgument() {
|
||||
MyService target = new MyService();
|
||||
Method method = getMethod(target, "addPeople");
|
||||
|
||||
validateArguments(target, method, new Object[] {List.of(faustino1234, cayetana6789)}, ex -> {
|
||||
|
||||
assertThat(ex.getConstraintViolations()).hasSize(2);
|
||||
assertThat(ex.getAllValidationResults()).hasSize(2);
|
||||
|
||||
int paramIndex = 0;
|
||||
String objectName = "people";
|
||||
List<ParameterErrors> results = ex.getBeanResults();
|
||||
|
||||
assertBeanResult(results.get(0), paramIndex, objectName, faustino1234, List.of(
|
||||
"""
|
||||
Field error in object 'people' on field 'name': rejected value [Faustino1234]; \
|
||||
codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [people.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]"""));
|
||||
|
||||
assertBeanResult(results.get(1), paramIndex, objectName, cayetana6789, List.of(
|
||||
"""
|
||||
Field error in object 'people' on field 'name': rejected value [Cayetana6789]; \
|
||||
codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [people.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]"""));
|
||||
});
|
||||
}
|
||||
|
||||
private void validateArguments(
|
||||
Object target, Method method, Object[] arguments, Consumer<MethodValidationException> assertions) {
|
||||
|
||||
MethodValidationAdapter adapter = new MethodValidationAdapter();
|
||||
|
||||
assertThatExceptionOfType(MethodValidationException.class)
|
||||
.isThrownBy(() -> adapter.validateMethodArguments(target, method, arguments, new Class<?>[0]))
|
||||
.satisfies(assertions);
|
||||
}
|
||||
|
||||
private void validateReturnValue(
|
||||
Object target, Method method, @Nullable Object returnValue, Consumer<MethodValidationException> assertions) {
|
||||
|
||||
MethodValidationAdapter adapter = new MethodValidationAdapter();
|
||||
|
||||
assertThatExceptionOfType(MethodValidationException.class)
|
||||
.isThrownBy(() -> adapter.validateMethodReturnValue(target, method, returnValue, new Class<?>[0]))
|
||||
.satisfies(assertions);
|
||||
}
|
||||
|
||||
private static void assertBeanResult(
|
||||
ParameterErrors errors, int parameterIndex, String objectName, Object argument,
|
||||
List<String> fieldErrors) {
|
||||
|
||||
assertThat(errors.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
|
||||
assertThat(errors.getObjectName()).isEqualTo(objectName);
|
||||
assertThat(errors.getArgument()).isSameAs(argument);
|
||||
|
||||
assertThat(errors.getFieldErrors())
|
||||
.extracting(FieldError::toString)
|
||||
.containsExactlyInAnyOrderElementsOf(fieldErrors);
|
||||
}
|
||||
|
||||
private static void assertValueResult(
|
||||
ParameterValidationResult result, int parameterIndex, Object argument, List<String> errors) {
|
||||
|
||||
assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
|
||||
assertThat(result.getArgument()).isEqualTo(argument);
|
||||
assertThat(result.getResolvableErrors())
|
||||
.extracting(MessageSourceResolvable::toString)
|
||||
.containsExactlyInAnyOrderElementsOf(errors);
|
||||
}
|
||||
|
||||
private static Method getMethod(Object target, String methodName) {
|
||||
return ClassUtils.getMethod(target.getClass(), methodName, (Class<?>[]) null);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class MyService {
|
||||
|
||||
public void addStudent(@Valid Person student, @Valid Person guardian, @Max(2) int degrees) {
|
||||
}
|
||||
|
||||
@Min(5)
|
||||
public int getIntValue() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Valid
|
||||
public Person getPerson() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void addPeople(@Valid List<Person> people) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private record Person(@Size(min = 1, max = 10) String name) {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue