Skip to content

Commit

Permalink
Refactoring in MethodValidationAdapter
Browse files Browse the repository at this point in the history
Extract the default logic for resolving the name of an @Valid
parameter into an ObjectNameResolver, and use it when there isn't
one configured.

See gh-30644
  • Loading branch information
rstoyanchev committed Jul 3, 2023
1 parent 7a79da5 commit 84e863f
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@
*/
public class MethodValidationAdapter implements MethodValidator {

private static final Comparator<ParameterValidationResult> RESULT_COMPARATOR = new ResultComparator();
private static final ObjectNameResolver defaultObjectNameResolver = new DefaultObjectNameResolver();

private static final MethodValidationResult EMPTY_RESULT = new EmptyMethodValidationResult();
private static final Comparator<ParameterValidationResult> resultComparator = new ResultComparator();

private static final MethodValidationResult emptyResult = new EmptyMethodValidationResult();


private final Supplier<Validator> validator;
Expand All @@ -83,8 +85,7 @@ public class MethodValidationAdapter implements MethodValidator {

private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

@Nullable
private BindingResultNameResolver objectNameResolver;
private ObjectNameResolver objectNameResolver = defaultObjectNameResolver;


/**
Expand Down Expand Up @@ -142,29 +143,43 @@ public MessageCodesResolver getMessageCodesResolver() {
}

/**
* Set the ParameterNameDiscoverer to use to resolve method parameter names
* that is in turn used to create error codes for {@link MessageSourceResolvable}.
* 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}.
* <p>Default is {@link org.springframework.core.DefaultParameterNameDiscoverer}.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}

/**
* Return the {@link #setParameterNameDiscoverer(ParameterNameDiscoverer) configured}
* Return the {@link #setParameterNameDiscoverer configured}
* {@code ParameterNameDiscoverer}.
*/
public ParameterNameDiscoverer getParameterNameDiscoverer() {
return this.parameterNameDiscoverer;
}

/**
* Configure a resolver for the name of Object parameters with nested errors
* to allow matching the name used in the higher level programming model,
* e.g. {@code @ModelAttribute} in Spring MVC.
* <p>If not configured, {@link #createBindingResult} determines the name.
* 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.
* <p>By default, the object name is resolved through:
* <ul>
* <li>{@link MethodParameter#getParameterName()} for input parameters
* <li>{@link Conventions#getVariableNameForReturnType(Method, Class, Object)}
* for a return type
* </ul>
* If a name cannot be determined, e.g. a return value with insufficient
* type information, then it defaults to one of:
* <ul>
* <li>{@code "{methodName}.arg{index}"} for input parameters
* <li>{@code "{methodName}.returnValue"} for a return type
* </ul>
*/
public void setBindingResultNameResolver(BindingResultNameResolver nameResolver) {
public void setObjectNameResolver(ObjectNameResolver nameResolver) {
this.objectNameResolver = nameResolver;
}

Expand Down Expand Up @@ -204,11 +219,11 @@ public final MethodValidationResult validateArguments(
invokeValidatorForArguments(target, method, arguments, groups);

if (violations.isEmpty()) {
return EMPTY_RESULT;
return emptyResult;
}

return adaptViolations(target, method, violations,
i -> parameters != null ? parameters[i] : new MethodParameter(method, i),
i -> parameters != null ? parameters[i] : initMethodParameter(method, i),
i -> arguments[i]);
}

Expand Down Expand Up @@ -242,11 +257,11 @@ public final MethodValidationResult validateReturnValue(
invokeValidatorForReturnValue(target, method, returnValue, groups);

if (violations.isEmpty()) {
return EMPTY_RESULT;
return emptyResult;
}

return adaptViolations(target, method, violations,
i -> returnType != null ? returnType : new MethodParameter(method, -1),
i -> returnType != null ? returnType : initMethodParameter(method, -1),
i -> returnValue);
}

Expand Down Expand Up @@ -284,7 +299,6 @@ else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
else {
continue;
}
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);

Object argument = argumentFunction.apply(parameter.getParameterIndex());
if (!itr.hasNext()) {
Expand All @@ -304,18 +318,17 @@ else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
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);
validatonResultList.sort(resultComparator);

return new DefaultMethodValidationResult(target, method, validatonResultList);
}

/**
* 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 MethodParameter initMethodParameter(Method method, int index) {
MethodParameter parameter = new MethodParameter(method, index);
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
return parameter;
}

private MessageSourceResolvable createMessageSourceResolvable(
Object target, MethodParameter parameter, ConstraintViolation<Object> violation) {

Expand All @@ -331,62 +344,24 @@ private MessageSourceResolvable createMessageSourceResolvable(
return new DefaultMessageSourceResolvable(codes, arguments, violation.getMessage());
}

/**
* Select an object name and create a {@link BindingResult} for the argument.
* You can configure a {@link #setBindingResultNameResolver(BindingResultNameResolver)
* bindingResultNameResolver} to determine in a way that matches the specific
* programming model, e.g. {@code @ModelAttribute} or {@code @RequestBody} arguments
* in Spring MVC.
* <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) {
String objectName = null;
if (this.objectNameResolver != null) {
objectName = this.objectNameResolver.resolveName(parameter, argument);
}
else {
if (parameter.getParameterIndex() != -1) {
objectName = parameter.getParameterName();
}
else {
try {
Method method = parameter.getMethod();
if (method != null) {
Class<?> containingClass = parameter.getContainingClass();
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
objectName = Conventions.getVariableNameForReturnType(method, resolvedType, argument);
}
}
catch (IllegalArgumentException ex) {
// insufficient type information
}
}
}
if (objectName == null) {
int index = parameter.getParameterIndex();
objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ""));
}
String objectName = this.objectNameResolver.resolveName(parameter, argument);
BeanPropertyBindingResult result = new BeanPropertyBindingResult(argument, objectName);
result.setMessageCodesResolver(this.messageCodesResolver);
return result;
}


/**
* Contract to determine the object name of an {@code @Valid} method parameter.
* Strategy to resolve the name of an {@code @Valid} method parameter to
* use for its {@link BindingResult}.
*/
public interface BindingResultNameResolver {
public interface ObjectNameResolver {

/**
* Determine the name for the given method parameter.
* Determine the name for the given method argument.
* @param parameter the method parameter
* @param value the argument or return value
* @param value the argument value or return value
* @return the name to use
*/
String resolveName(MethodParameter parameter, @Nullable Object value);
Expand Down Expand Up @@ -484,6 +459,40 @@ public ParameterErrors build() {
}


/**
* Default algorithm to select an object name, as described in
* {@link #setObjectNameResolver(ObjectNameResolver)}.
*/
private static class DefaultObjectNameResolver implements ObjectNameResolver {

@Override
public String resolveName(MethodParameter parameter, @Nullable Object value) {
String objectName = null;
if (parameter.getParameterIndex() != -1) {
objectName = parameter.getParameterName();
}
else {
try {
Method method = parameter.getMethod();
if (method != null) {
Class<?> containingClass = parameter.getContainingClass();
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
objectName = Conventions.getVariableNameForReturnType(method, resolvedType, value);
}
}
catch (IllegalArgumentException ex) {
// insufficient type information
}
}
if (objectName == null) {
int index = parameter.getParameterIndex();
objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ".returnValue"));
}
return objectName;
}
}


/**
* Comparator for validation results, sorted by method parameter index first,
* also falling back on container indexes if necessary for cascaded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ void validateArgumentWithCustomObjectName() {
MyService target = new MyService();
Method method = getMethod(target, "addStudent");

this.validationAdapter.setBindingResultNameResolver((parameter, value) -> "studentToAdd");
this.validationAdapter.setObjectNameResolver((param, value) -> "studentToAdd");

testArgs(target, method, new Object[] {faustino1234, new Person("Joe"), 1}, ex -> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
import org.springframework.web.bind.support.WebBindingInitializer;

/**
* {@link org.springframework.validation.beanvalidation.MethodValidator} for
* {@code @RequestMapping} methods.
* {@link org.springframework.validation.beanvalidation.MethodValidator} that
* uses Bean Validation to validate {@code @RequestMapping} method arguments.
*
* <p>Handles validation results by populating {@link BindingResult} method
* arguments with errors from {@link MethodValidationResult#getBeanResults()
Expand All @@ -49,6 +49,9 @@
*/
public final class HandlerMethodValidator implements MethodValidator {

private static final MethodValidationAdapter.ObjectNameResolver objectNameResolver = new WebObjectNameResolver();


private final MethodValidationAdapter validationAdapter;


Expand Down Expand Up @@ -119,43 +122,51 @@ public MethodValidationResult validateReturnValue(Object target, Method method,
return this.validationAdapter.validateReturnValue(target, method, returnType, returnValue, groups);
}

private static String determineObjectName(MethodParameter param, @Nullable Object argument) {
if (param.hasParameterAnnotation(RequestBody.class) || param.hasParameterAnnotation(RequestPart.class)) {
return Conventions.getVariableNameForParameter(param);
}
else {
return (param.getParameterIndex() != -1 ?
ModelFactory.getNameForParameter(param) :
ModelFactory.getNameForReturnValue(argument, param));
}
}


/**
* Static factory method to create a {@link HandlerMethodValidator} if Bean
* Validation is enabled in Spring MVC or WebFlux.
* Static factory method to create a {@link HandlerMethodValidator} when Bean
* Validation is enabled for use via {@link ConfigurableWebBindingInitializer},
* for example in Spring MVC or WebFlux config.
*/
@Nullable
public static MethodValidator from(
@Nullable WebBindingInitializer bindingInitializer,
@Nullable ParameterNameDiscoverer parameterNameDiscoverer) {
@Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer) {

if (bindingInitializer instanceof ConfigurableWebBindingInitializer configurableInitializer) {
if (initializer instanceof ConfigurableWebBindingInitializer configurableInitializer) {
if (configurableInitializer.getValidator() instanceof Validator validator) {
MethodValidationAdapter adapter = new MethodValidationAdapter(validator);
if (parameterNameDiscoverer != null) {
adapter.setParameterNameDiscoverer(parameterNameDiscoverer);
if (paramNameDiscoverer != null) {
adapter.setParameterNameDiscoverer(paramNameDiscoverer);
}
MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver();
if (codesResolver != null) {
adapter.setMessageCodesResolver(codesResolver);
}
HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter);
adapter.setBindingResultNameResolver(HandlerMethodValidator::determineObjectName);
adapter.setObjectNameResolver(objectNameResolver);
return methodValidator;
}
}
return null;
}


/**
* ObjectNameResolver for web controller methods.
*/
private static class WebObjectNameResolver implements MethodValidationAdapter.ObjectNameResolver {

@Override
public String resolveName(MethodParameter param, @Nullable Object value) {
if (param.hasParameterAnnotation(RequestBody.class) || param.hasParameterAnnotation(RequestPart.class)) {
return Conventions.getVariableNameForParameter(param);
}
else {
return (param.getParameterIndex() != -1 ?
ModelFactory.getNameForParameter(param) :
ModelFactory.getNameForReturnValue(value, param));
}
}
}

}

0 comments on commit 84e863f

Please sign in to comment.