From 5c5d8e61aecbdfa3c7bbe48065c4425f60671a6d Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 7 Jun 2023 15:11:21 +0100 Subject: [PATCH] Add BindingResultNameResolver option See gh-29825 --- .../MethodValidationAdapter.java | 68 +++++++++++++++---- .../MethodValidationAdapterTests.java | 31 +++++++-- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index 6d9180850ca..c2282a7dc25 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -85,6 +85,9 @@ public class MethodValidationAdapter { private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + @Nullable + private BindingResultNameResolver objectNameResolver; + /** * Create an instance using a default JSR-303 validator underneath. @@ -157,6 +160,19 @@ public class MethodValidationAdapter { return this.parameterNameDiscoverer; } + /** + * Configure a resolver for {@link BindingResult} method parameters to match + * the behavior of the higher level programming model, e.g. how the name of + * {@code @ModelAttribute} or {@code @RequestBody} is determined in Spring MVC. + *

If this is not configured, then {@link #createBindingResult} will apply + * default behavior to resolve the name to use. + * behavior applies. + * @param nameResolver the resolver to use + */ + public void setBindingResultNameResolver(BindingResultNameResolver nameResolver) { + this.objectNameResolver = nameResolver; + } + /** * Use this method determine the validation groups to pass into @@ -307,6 +323,10 @@ public class MethodValidationAdapter { /** * 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. *

By default, the name is based on the parameter name, or for a return type on * {@link Conventions#getVariableNameForReturnType(Method, Class, Object)}. *

If a name cannot be determined for any reason, e.g. a return value with @@ -316,22 +336,30 @@ public class MethodValidationAdapter { * @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); - } + String objectName = null; + if (this.objectNameResolver != null) { + objectName = this.objectNameResolver.resolveName(parameter, argument); + } + else { + if (parameter.getParameterIndex() != -1) { + objectName = parameter.getParameterName(); } - catch (IllegalArgumentException ex) { - // insufficient type information + 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 : "")); } BeanPropertyBindingResult result = new BeanPropertyBindingResult(argument, objectName); @@ -340,6 +368,22 @@ public class MethodValidationAdapter { } + /** + * Contract to determine the object name of an {@code @Valid} method parameter. + */ + public interface BindingResultNameResolver { + + /** + * Determine the name for the given method parameter. + * @param parameter the method parameter + * @param value the argument or return value + * @return the name to use + */ + String resolveName(MethodParameter parameter, @Nullable Object value); + + } + + /** * Builds a validation result for a value method parameter with constraints * declared directly on it. diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java index df10ce558a1..239abf0d792 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java @@ -39,13 +39,14 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class MethodValidationAdapterTests { - private static final MethodValidationAdapter validationAdapter = new MethodValidationAdapter(); - private static final Person faustino1234 = new Person("Faustino1234"); private static final Person cayetana6789 = new Person("Cayetana6789"); + private final MethodValidationAdapter validationAdapter = new MethodValidationAdapter(); + + @Test void validateArguments() { MyService target = new MyService(); @@ -83,6 +84,28 @@ public class MethodValidationAdapterTests { }); } + @Test + void validateArgumentWithCustomObjectName() { + MyService target = new MyService(); + Method method = getMethod(target, "addStudent"); + + this.validationAdapter.setBindingResultNameResolver((parameter, value) -> "studentToAdd"); + + validateArguments(target, method, new Object[] {faustino1234, new Person("Joe"), 1}, ex -> { + + assertThat(ex.getConstraintViolations()).hasSize(1); + assertThat(ex.getAllValidationResults()).hasSize(1); + + assertBeanResult(ex.getBeanResults().get(0), 0, "studentToAdd", faustino1234, List.of( + """ + Field error in object 'studentToAdd' on field 'name': rejected value [Faustino1234]; \ + codes [Size.studentToAdd.name,Size.name,Size.java.lang.String,Size]; \ + arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ + codes [studentToAdd.name,name]; arguments []; default message [name],10,1]; \ + default message [size must be between 1 and 10]""")); + }); + } + @Test void validateReturnValue() { MyService target = new MyService(); @@ -158,14 +181,14 @@ public class MethodValidationAdapterTests { Object target, Method method, Object[] arguments, Consumer assertions) { assertions.accept( - validationAdapter.validateMethodArguments(target, method, arguments, new Class[0])); + this.validationAdapter.validateMethodArguments(target, method, arguments, new Class[0])); } private void validateReturnValue( Object target, Method method, @Nullable Object returnValue, Consumer assertions) { assertions.accept( - validationAdapter.validateMethodReturnValue(target, method, returnValue, new Class[0])); + this.validationAdapter.validateMethodReturnValue(target, method, returnValue, new Class[0])); } private static void assertBeanResult(