Add BindingResultNameResolver option

See gh-29825
This commit is contained in:
rstoyanchev 2023-06-07 15:11:21 +01:00
parent 0fef380202
commit 5c5d8e61ae
2 changed files with 83 additions and 16 deletions

View File

@ -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.
* <p>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.
* <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
@ -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.

View File

@ -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<MethodValidationResult> 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<MethodValidationResult> assertions) {
assertions.accept(
validationAdapter.validateMethodReturnValue(target, method, returnValue, new Class<?>[0]));
this.validationAdapter.validateMethodReturnValue(target, method, returnValue, new Class<?>[0]));
}
private static void assertBeanResult(