Revert use of leafBean in MethodValidationAdapter
The goal for #31530 was to support bean validation on Set and other method parameters that are containers of value(s) for which there is a registered Jakarta Validation ValueExtractor. Unfortunately, bean validation does not expose the unwrapped value for a Path.Node, which we need for a method parameter in order to create a BindingResult for the specific bean within the container, and the leafBean that we tried to use is really the node at the very bottom of the property path (i.e. not what we need). This change removes the use of beanLeaf, restores the logic as it was before, adds support for arrays, and a new test class for scenarios with cascaded violations. See gh-31746
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.method.MethodValidationResult;
|
||||
import org.springframework.validation.method.ParameterErrors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test method validation scenarios with cascaded violations on different types
|
||||
* of method parameters and return values.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class MethodValidationAdapterPropertyPathTests {
|
||||
|
||||
private static final Person validPerson = new Person("John");
|
||||
|
||||
private static final Person invalidPerson = new Person("Long John Silver");
|
||||
|
||||
private static final Class<?>[] HINTS = new Class<?>[0];
|
||||
|
||||
|
||||
private final MethodValidationAdapter validationAdapter = new MethodValidationAdapter();
|
||||
|
||||
|
||||
@Nested
|
||||
class ArgumentTests {
|
||||
|
||||
@Test
|
||||
void fieldOfObjectPropertyOfBean() {
|
||||
Method method = getMethod("addCourse");
|
||||
Object[] args = {new Course("CS 101", invalidPerson, Collections.emptyList())};
|
||||
|
||||
MethodValidationResult result =
|
||||
validationAdapter.validateArguments(new MyService(), method, null, args, HINTS);
|
||||
|
||||
assertThat(result.getAllErrors()).hasSize(1);
|
||||
ParameterErrors errors = result.getBeanResults().get(0);
|
||||
assertSingleFieldError(errors, 1, null, null, null, "professor.name", invalidPerson.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldOfObjectPropertyOfListElement() {
|
||||
Method method = getMethod("addCourseList");
|
||||
List<Course> courses = List.of(new Course("CS 101", invalidPerson, Collections.emptyList()));
|
||||
|
||||
MethodValidationResult result = validationAdapter.validateArguments(
|
||||
new MyService(), method, null, new Object[] {courses}, HINTS);
|
||||
|
||||
assertThat(result.getAllErrors()).hasSize(1);
|
||||
ParameterErrors errors = result.getBeanResults().get(0);
|
||||
assertSingleFieldError(errors, 1, courses, 0, null, "professor.name", invalidPerson.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldOfObjectPropertyOfListElements() {
|
||||
Method method = getMethod("addCourseList");
|
||||
List<Course> courses = List.of(
|
||||
new Course("CS 101", invalidPerson, Collections.emptyList()),
|
||||
new Course("CS 102", invalidPerson, Collections.emptyList()));
|
||||
|
||||
MethodValidationResult result = validationAdapter.validateArguments(
|
||||
new MyService(), method, null, new Object[] {courses}, HINTS);
|
||||
|
||||
assertThat(result.getAllErrors()).hasSize(2);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
ParameterErrors errors = result.getBeanResults().get(i);
|
||||
assertThat(errors.getContainerIndex()).isEqualTo(i);
|
||||
assertThat(errors.getFieldError().getField()).isEqualTo("professor.name");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldOfObjectPropertyUnderListPropertyOfListElement() {
|
||||
Method method = getMethod("addCourseList");
|
||||
Course cs101 = new Course("CS 101", invalidPerson, Collections.emptyList());
|
||||
Course cs201 = new Course("CS 201", validPerson, List.of(cs101));
|
||||
Course cs301 = new Course("CS 301", validPerson, List.of(cs201));
|
||||
List<Course> courses = List.of(cs301);
|
||||
Object[] args = {courses};
|
||||
|
||||
MethodValidationResult result =
|
||||
validationAdapter.validateArguments(new MyService(), method, null, args, HINTS);
|
||||
|
||||
assertThat(result.getAllErrors()).hasSize(1);
|
||||
ParameterErrors errors = result.getBeanResults().get(0);
|
||||
|
||||
assertSingleFieldError(errors, 1, courses, 0, null,
|
||||
"requiredCourses[0].requiredCourses[0].professor.name", invalidPerson.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldOfObjectPropertyOfArrayElement() {
|
||||
Method method = getMethod("addCourseArray");
|
||||
Course[] courses = new Course[] {new Course("CS 101", invalidPerson, Collections.emptyList())};
|
||||
|
||||
MethodValidationResult result = validationAdapter.validateArguments(
|
||||
new MyService(), method, null, new Object[] {courses}, HINTS);
|
||||
|
||||
assertThat(result.getAllErrors()).hasSize(1);
|
||||
ParameterErrors errors = result.getBeanResults().get(0);
|
||||
assertSingleFieldError(errors, 1, courses, 0, null, "professor.name", invalidPerson.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldOfObjectPropertyOfMapValue() {
|
||||
Method method = getMethod("addCourseMap");
|
||||
Map<String, Course> courses = Map.of("CS 101", new Course("CS 101", invalidPerson, Collections.emptyList()));
|
||||
|
||||
MethodValidationResult result = validationAdapter.validateArguments(
|
||||
new MyService(), method, null, new Object[] {courses}, HINTS);
|
||||
|
||||
assertThat(result.getAllErrors()).hasSize(1);
|
||||
ParameterErrors errors = result.getBeanResults().get(0);
|
||||
assertSingleFieldError(errors, 1, courses, null, "CS 101", "professor.name", invalidPerson.name());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class ReturnValueTests {
|
||||
|
||||
@Test
|
||||
void fieldOfObjectPropertyOfBean() {
|
||||
Method method = getMethod("getCourse");
|
||||
Course course = new Course("CS 101", invalidPerson, Collections.emptyList());
|
||||
|
||||
MethodValidationResult result =
|
||||
validationAdapter.validateReturnValue(new MyService(), method, null, course, HINTS);
|
||||
|
||||
assertThat(result.getAllErrors()).hasSize(1);
|
||||
ParameterErrors errors = result.getBeanResults().get(0);
|
||||
assertSingleFieldError(errors, 1, null, null, null, "professor.name", invalidPerson.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldOfObjectPropertyOfListElement() {
|
||||
Method method = getMethod("addCourseList");
|
||||
List<Course> courses = List.of(new Course("CS 101", invalidPerson, Collections.emptyList()));
|
||||
|
||||
MethodValidationResult result = validationAdapter.validateArguments(
|
||||
new MyService(), method, null, new Object[] {courses}, HINTS);
|
||||
|
||||
assertThat(result.getAllErrors()).hasSize(1);
|
||||
ParameterErrors errors = result.getBeanResults().get(0);
|
||||
assertSingleFieldError(errors, 1, courses, 0, null, "professor.name", invalidPerson.name());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void assertSingleFieldError(
|
||||
ParameterErrors errors, int errorCount,
|
||||
@Nullable Object container, @Nullable Integer index, @Nullable Object key,
|
||||
String field, Object rejectedValue) {
|
||||
|
||||
assertThat(errors.getErrorCount()).isEqualTo(errorCount);
|
||||
assertThat(errors.getErrorCount()).isEqualTo(1);
|
||||
assertThat(errors.getContainer()).isEqualTo(container);
|
||||
assertThat(errors.getContainerIndex()).isEqualTo(index);
|
||||
assertThat(errors.getContainerKey()).isEqualTo(key);
|
||||
|
||||
FieldError fieldError = errors.getFieldError();
|
||||
assertThat(fieldError).isNotNull();
|
||||
assertThat(fieldError.getField()).isEqualTo(field);
|
||||
assertThat(fieldError.getRejectedValue()).isEqualTo(rejectedValue);
|
||||
}
|
||||
|
||||
|
||||
private static Method getMethod(String methodName) {
|
||||
return ClassUtils.getMethod(MyService.class, methodName, (Class<?>[]) null);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class MyService {
|
||||
|
||||
public void addCourse(@Valid Course course) {
|
||||
}
|
||||
|
||||
public void addCourseList(@Valid List<Course> courses) {
|
||||
}
|
||||
|
||||
public void addCourseArray(@Valid Course[] courses) {
|
||||
}
|
||||
|
||||
public void addCourseMap(@Valid Map<String, Course> courses) {
|
||||
}
|
||||
|
||||
@Valid
|
||||
public Course getCourse(Course course) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Valid
|
||||
public List<Course> getCourseList() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private record Course(@NotBlank String title, @Valid Person professor, @Valid List<Course> requiredCourses) {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private record Person(@Size(min = 1, max = 5) String name) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,10 +17,8 @@
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
@@ -197,42 +195,6 @@ public class MethodValidationAdapterTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateSetArgument() {
|
||||
MyService target = new MyService();
|
||||
Method method = getMethod(target, "addPeople");
|
||||
|
||||
testArgs(target, method, new Object[] {Set.of(faustino1234, cayetana6789)}, ex -> {
|
||||
|
||||
assertThat(ex.getAllValidationResults()).hasSize(2);
|
||||
|
||||
int paramIndex = 0;
|
||||
String objectName = "people";
|
||||
List<ParameterErrors> results = ex.getBeanResults();
|
||||
|
||||
assertThat(results).satisfiesExactlyInAnyOrder(
|
||||
result -> assertBeanResult(result, paramIndex, objectName, faustino1234, List.of("""
|
||||
Field error in object 'people' on field 'name': rejected value [Faustino1234]; \
|
||||
codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [people.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]""")),
|
||||
result -> assertBeanResult(result, paramIndex, objectName, cayetana6789, List.of("""
|
||||
Field error in object 'people' on field 'name': rejected value [Cayetana6789]; \
|
||||
codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [people.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]""", """
|
||||
Field error in object 'people' on field 'hobbies[0]': rejected value [ ]; \
|
||||
codes [NotBlank.people.hobbies[0],NotBlank.people.hobbies,NotBlank.hobbies[0],\
|
||||
NotBlank.hobbies,NotBlank.java.lang.String,NotBlank]; arguments \
|
||||
[org.springframework.context.support.DefaultMessageSourceResolvable: codes \
|
||||
[people.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \
|
||||
default message [must not be blank]"""))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private void testArgs(Object target, Method method, Object[] args, Consumer<MethodValidationResult> consumer) {
|
||||
consumer.accept(this.validationAdapter.validateArguments(target, method, null, args, new Class<?>[0]));
|
||||
}
|
||||
@@ -285,7 +247,7 @@ public class MethodValidationAdapterTests {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void addPeople(@Valid Collection<Person> people) {
|
||||
public void addPeople(@Valid List<Person> people) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user