Provide access to underlying ConstraintViolation

Closes gh-33025
This commit is contained in:
rstoyanchev 2024-07-12 14:43:12 +01:00
parent 3e48498663
commit bd31e8dacc
7 changed files with 87 additions and 15 deletions

View File

@ -50,6 +50,7 @@ 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;
@ -402,7 +403,7 @@ public class MethodValidationAdapter implements MethodValidator {
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());
return new ViolationMessageSourceResolvable(codes, arguments, violation.getMessage(), violation);
}
private BindingResult createBindingResult(MethodParameter parameter, @Nullable Object argument) {
@ -472,7 +473,11 @@ public class MethodValidationAdapter implements MethodValidator {
public ParameterValidationResult build() {
return new ParameterValidationResult(
this.parameter, this.value, this.resolvableErrors, this.container,
this.containerIndex, this.containerKey);
this.containerIndex, this.containerKey,
(error, sourceType) -> {
Assert.isTrue(sourceType.equals(ConstraintViolation.class), "Unexpected source type");
return ((ViolationMessageSourceResolvable) error).getViolation();
});
}
}
@ -526,6 +531,24 @@ public class MethodValidationAdapter implements MethodValidator {
}
@SuppressWarnings("serial")
private static class ViolationMessageSourceResolvable extends DefaultMessageSourceResolvable {
private final transient ConstraintViolation<Object> violation;
public ViolationMessageSourceResolvable(
String[] codes, Object[] arguments, String defaultMessage, ConstraintViolation<Object> violation) {
super(codes, arguments, defaultMessage);
this.violation = violation;
}
public ConstraintViolation<Object> getViolation() {
return this.violation;
}
}
/**
* Default algorithm to select an object name, as described in {@link #setObjectNameResolver}.
*/

View File

@ -47,7 +47,9 @@ public class ParameterErrors extends ParameterValidationResult implements Errors
MethodParameter parameter, @Nullable Object argument, Errors errors,
@Nullable Object container, @Nullable Integer index, @Nullable Object key) {
super(parameter, argument, errors.getAllErrors(), container, index, key);
super(parameter, argument, errors.getAllErrors(),
container, index, key, (error, sourceType) -> ((FieldError) error).unwrap(sourceType));
this.errors = errors;
}

View File

@ -18,6 +18,8 @@ package org.springframework.validation.method;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.core.MethodParameter;
@ -62,13 +64,16 @@ public class ParameterValidationResult {
@Nullable
private final Object containerKey;
private final BiFunction<MessageSourceResolvable, Class<?>, Object> sourceLookup;
/**
* Create a {@code ParameterValidationResult}.
*/
public ParameterValidationResult(
MethodParameter param, @Nullable Object arg, Collection<? extends MessageSourceResolvable> errors,
@Nullable Object container, @Nullable Integer index, @Nullable Object key) {
@Nullable Object container, @Nullable Integer index, @Nullable Object key,
BiFunction<MessageSourceResolvable, Class<?>, Object> sourceLookup) {
Assert.notNull(param, "MethodParameter is required");
Assert.notEmpty(errors, "`resolvableErrors` must not be empty");
@ -78,18 +83,36 @@ public class ParameterValidationResult {
this.container = container;
this.containerIndex = index;
this.containerKey = key;
this.sourceLookup = sourceLookup;
}
/**
* Create a {@code ParameterValidationResult}.
* @deprecated in favor of
* {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object)}
* {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object, Function)}
*/
@Deprecated(since = "6.2", forRemoval = true)
public ParameterValidationResult(
MethodParameter param, @Nullable Object arg, Collection<? extends MessageSourceResolvable> errors,
@Nullable Object container, @Nullable Integer index, @Nullable Object key) {
this(param, arg, errors, container, index, key, (error, sourceType) -> {
throw new IllegalArgumentException("No source object of the given type");
});
}
/**
* Create a {@code ParameterValidationResult}.
* @deprecated in favor of
* {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object, Function)}
*/
@Deprecated(since = "6.1.3", forRemoval = true)
public ParameterValidationResult(
MethodParameter param, @Nullable Object arg, Collection<? extends MessageSourceResolvable> errors) {
this(param, arg, errors, null, null, null);
this(param, arg, errors, null, null, null, (error, sourceType) -> {
throw new IllegalArgumentException("No source object of the given type");
});
}
@ -164,6 +187,17 @@ public class ParameterValidationResult {
return this.containerKey;
}
/**
* Unwrap the source behind the given error. For Jakarta Bean validation the
* source is a {@link jakarta.validation.ConstraintViolation}.
* @param sourceType the expected source type
* @return the source object of the given type
* @since 6.2
*/
@SuppressWarnings("unchecked")
public <T> T unwrap(MessageSourceResolvable error, Class<T> sourceType) {
return (T) this.sourceLookup.apply(error, sourceType);
}
@Override
public boolean equals(@Nullable Object other) {

View File

@ -22,6 +22,7 @@ import java.util.Locale;
import java.util.Set;
import java.util.function.Consumer;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
@ -99,7 +100,7 @@ class MethodValidationAdapterTests {
default message [must not be blank]"""));
assertValueResult(ex.getValueResults().get(0), 2, 3, List.of("""
org.springframework.context.support.DefaultMessageSourceResolvable: \
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
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]; \
@ -136,7 +137,7 @@ class MethodValidationAdapterTests {
assertThat(ex.getAllValidationResults()).hasSize(1);
assertValueResult(ex.getValueResults().get(0), -1, 4, List.of("""
org.springframework.context.support.DefaultMessageSourceResolvable: \
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
codes [Min.myService#getIntValue,Min,Min.int]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [myService#getIntValue]; arguments []; default message [],5]; \
@ -204,7 +205,7 @@ class MethodValidationAdapterTests {
testArgs(target, method, new Object[] {List.of(" ")}, ex -> {
assertThat(ex.getAllValidationResults()).hasSize(1);
assertValueResult(ex.getValueResults().get(0), 0, " ", List.of("""
org.springframework.context.support.DefaultMessageSourceResolvable: \
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
codes [NotBlank.myService#addHobbies.hobbies,NotBlank.hobbies,NotBlank.java.util.List,NotBlank]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [myService#addHobbies.hobbies,hobbies]; \
@ -220,7 +221,7 @@ class MethodValidationAdapterTests {
testArgs(target, method, new Object[] {Set.of("test", " ")}, ex -> {
assertThat(ex.getAllValidationResults()).hasSize(1);
assertValueResult(ex.getValueResults().get(0), 0, Set.of("test", " "), List.of("""
org.springframework.context.support.DefaultMessageSourceResolvable: \
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
codes [NotBlank.myService#addUniqueHobbies.hobbies,NotBlank.hobbies,NotBlank.java.util.Set,NotBlank]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [myService#addUniqueHobbies.hobbies,hobbies]; \
@ -254,9 +255,14 @@ class MethodValidationAdapterTests {
assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
assertThat(result.getArgument()).isEqualTo(argument);
assertThat(result.getResolvableErrors())
List<MessageSourceResolvable> resolvableErrors = result.getResolvableErrors();
assertThat(resolvableErrors)
.extracting(MessageSourceResolvable::toString)
.containsExactlyInAnyOrderElementsOf(errors);
resolvableErrors.forEach(error ->
assertThat(result.unwrap(error, ConstraintViolation.class)).isNotNull());
}
private static Method getMethod(Object target, String methodName) {

View File

@ -128,7 +128,8 @@ class HandlerMethodValidationExceptionTests {
}
else {
MessageSourceResolvable error = new DefaultMessageSourceResolvable("Size");
return new ParameterValidationResult(param, "123", List.of(error), null, null, null);
return new ParameterValidationResult(
param, "123", List.of(error), null, null, null, (e, t) -> null);
}
})
.toList());

View File

@ -220,7 +220,7 @@ class MethodValidationTests {
assertValueResult(ex.getValueResults().get(0), 2, "123", Collections.singletonList(
"""
org.springframework.context.support.DefaultMessageSourceResolvable: \
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
codes [Size.validController#handle.myHeader,Size.myHeader,Size.java.lang.String,Size]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [validController#handle.myHeader,myHeader]; arguments []; default message [myHeader],10,5]; \
@ -360,9 +360,15 @@ class MethodValidationTests {
assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
assertThat(result.getArgument()).isEqualTo(argument);
assertThat(result.getResolvableErrors())
List<MessageSourceResolvable> resolvableErrors = result.getResolvableErrors();
assertThat(resolvableErrors)
.extracting(MessageSourceResolvable::toString)
.containsExactlyInAnyOrderElementsOf(errors);
resolvableErrors.forEach(error ->
assertThat(result.unwrap(error, ConstraintViolation.class)).isNotNull());
}

View File

@ -183,7 +183,7 @@ class MethodValidationTests {
));
assertValueResult(ex.getValueResults().get(0), 2, "123", List.of("""
org.springframework.context.support.DefaultMessageSourceResolvable: \
org.springframework.validation.beanvalidation.MethodValidationAdapter$ViolationMessageSourceResolvable: \
codes [Size.validController#handle.myHeader,Size.myHeader,Size.java.lang.String,Size]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [validController#handle.myHeader,myHeader]; arguments []; default message [myHeader],10,5]; \