org.springframework.validation.beanvalidation.MethodValidationAdapter Maven / Gradle / Ivy
/*
* 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.
* 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.Collections;
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.Optional;
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.Assert;
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;
import org.springframework.validation.method.MethodValidationResult;
import org.springframework.validation.method.MethodValidator;
import org.springframework.validation.method.ParameterErrors;
import org.springframework.validation.method.ParameterValidationResult;
/**
* {@link MethodValidator} that uses a Bean Validation
* {@link jakarta.validation.Validator} for validation, and adapts
* {@link ConstraintViolation}s to {@link MethodValidationResult}.
*
* @author Rossen Stoyanchev
* @since 6.1
*/
public class MethodValidationAdapter implements MethodValidator {
private static final MethodValidationResult emptyValidationResult = MethodValidationResult.emptyResult();
private static final ObjectNameResolver defaultObjectNameResolver = new DefaultObjectNameResolver();
private static final Comparator resultComparator = new ResultComparator();
private final Supplier validator;
private final Supplier validatorAdapter;
private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private ObjectNameResolver objectNameResolver = defaultObjectNameResolver;
/**
* Create an instance using a default JSR-303 validator underneath.
*/
public MethodValidationAdapter() {
this.validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator());
this.validatorAdapter = initValidatorAdapter(this.validator);
}
/**
* Create an instance using the given JSR-303 ValidatorFactory.
* @param validatorFactory the JSR-303 ValidatorFactory to use
*/
@SuppressWarnings("DataFlowIssue")
public MethodValidationAdapter(ValidatorFactory validatorFactory) {
if (validatorFactory instanceof SpringValidatorAdapter adapter) {
this.validator = () -> adapter;
this.validatorAdapter = () -> adapter;
}
else {
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 = initValidatorAdapter(this.validator);
}
/**
* Create an instance for the supplied (potentially lazily initialized) Validator.
* @param validator a Supplier for the Validator to use
*/
public MethodValidationAdapter(Supplier validator) {
this.validator = validator;
this.validatorAdapter = initValidatorAdapter(validator);
}
private static Supplier initValidatorAdapter(Supplier validatorSupplier) {
return SingletonSupplier.of(() -> {
Validator validator = validatorSupplier.get();
return (validator instanceof SpringValidatorAdapter sva ? sva : new SpringValidatorAdapter(validator));
});
}
/**
* Return the {@link SpringValidatorAdapter} configured for use.
*/
public Supplier getSpringValidatorAdapter() {
return this.validatorAdapter;
}
/**
* Set the strategy to use to determine message codes for violations.
* 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 {@code ParameterNameDiscoverer} to discover method parameter names
* with to create error codes for {@link MessageSourceResolvable}. Used only
* when {@link MethodParameter}s are not passed into
* {@link #validateArguments} or {@link #validateReturnValue}.
*
Default is {@link org.springframework.core.DefaultParameterNameDiscoverer}.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
/**
* Return the {@link #setParameterNameDiscoverer configured}
* {@code ParameterNameDiscoverer}.
*/
public ParameterNameDiscoverer getParameterNameDiscoverer() {
return this.parameterNameDiscoverer;
}
/**
* Configure a resolver to determine the name of an {@code @Valid} method
* parameter to use for its {@link BindingResult}. This allows aligning with
* a higher level programming model such as to resolve the name of an
* {@code @ModelAttribute} method parameter in Spring MVC.
*
By default, the object name is resolved through:
*
* - {@link MethodParameter#getParameterName()} for input parameters
*
- {@link Conventions#getVariableNameForReturnType(Method, Class, Object)}
* for a return type
*
* If a name cannot be determined, for example, a return value with insufficient
* type information, then it defaults to one of:
*
* - {@code "{methodName}.arg{index}"} for input parameters
*
- {@code "{methodName}.returnValue"} for a return type
*
*/
public void setObjectNameResolver(ObjectNameResolver nameResolver) {
this.objectNameResolver = nameResolver;
}
/**
* {@inheritDoc}
* 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.
*/
@Override
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]);
}
@Override
public final MethodValidationResult validateArguments(
Object target, Method method, @Nullable MethodParameter[] parameters,
Object[] arguments, Class>[] groups) {
Set> violations =
invokeValidatorForArguments(target, method, arguments, groups);
if (violations.isEmpty()) {
return emptyValidationResult;
}
return adaptViolations(target, method, violations,
i -> (parameters != null ? parameters[i] : initMethodParameter(method, i)),
i -> arguments[i]);
}
/**
* Invoke the validator, and return the resulting violations.
*/
public final Set> invokeValidatorForArguments(
Object target, Method method, Object[] arguments, Class>[] groups) {
ExecutableValidator execVal = this.validator.get().forExecutables();
try {
return 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 bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(method, target.getClass());
return execVal.validateParameters(target, bridgedMethod, arguments, groups);
}
}
@Override
public final MethodValidationResult validateReturnValue(
Object target, Method method, @Nullable MethodParameter returnType,
@Nullable Object returnValue, Class>[] groups) {
Set> violations =
invokeValidatorForReturnValue(target, method, returnValue, groups);
if (violations.isEmpty()) {
return emptyValidationResult;
}
return adaptViolations(target, method, violations,
i -> (returnType != null ? returnType : initMethodParameter(method, -1)),
i -> returnValue);
}
/**
* Invoke the validator, and return the resulting violations.
*/
public final Set> invokeValidatorForReturnValue(
Object target, Method method, @Nullable Object returnValue, Class>[] groups) {
ExecutableValidator execVal = this.validator.get().forExecutables();
return execVal.validateReturnValue(target, method, returnValue, groups);
}
private MethodValidationResult adaptViolations(
Object target, Method method, Set> violations,
Function parameterFunction,
Function argumentFunction) {
Map paramViolations = new LinkedHashMap<>();
Map nestedViolations = new LinkedHashMap<>();
List crossParamErrors = null;
for (ConstraintViolation