Merge branch '6.2.x'

This commit is contained in:
rstoyanchev
2024-12-30 14:48:04 +00:00
4 changed files with 124 additions and 5 deletions

View File

@@ -1032,7 +1032,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
List<V> list = (List<V>) CollectionFactory.createCollection(paramType, size);
indexes.forEach(i -> list.add(null));
for (int i = 0; i < size; i++) {
list.add(null);
}
for (int index : indexes) {
list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver));
}

View File

@@ -367,7 +367,7 @@ public class MethodValidationAdapter implements MethodValidator {
container = null;
}
if (node.getKind().equals(ElementKind.PROPERTY)) {
if (node.getKind().equals(ElementKind.PROPERTY) || node.getKind().equals(ElementKind.BEAN)) {
nestedViolations
.computeIfAbsent(parameterNode, k ->
new ParamErrorsBuilder(parameter, value, container, index, key))

View File

@@ -121,6 +121,24 @@ class DataBinderConstructTests {
assertThat(list.get(2).param1()).isEqualTo("value3");
}
@Test // gh-34145
void listBindingWithNonconsecutiveIndices() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
"dataClassList[3].param1", "value3", "dataClassList[3].param2", "true"));
DataBinder binder = initDataBinder(ListDataClass.class);
binder.construct(valueResolver);
ListDataClass dataClass = getTarget(binder);
List<DataClass> list = dataClass.dataClassList();
assertThat(list.get(0).param1()).isEqualTo("value1");
assertThat(list.get(1).param1()).isEqualTo("value2");
assertThat(list.get(3).param1()).isEqualTo("value3");
}
@Test
void mapBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(

View File

@@ -16,19 +16,32 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.ConstraintValidatorFactory;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Payload;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import jakarta.validation.executable.ExecutableValidator;
import jakarta.validation.metadata.BeanDescriptor;
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -39,11 +52,12 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import org.springframework.validation.method.ParameterErrors;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
@@ -91,13 +105,17 @@ class MethodValidationTests {
private InvocationCountingValidator jakartaValidator;
private final TestConstraintValidator testConstraintValidator = new TestConstraintValidator();
@BeforeEach
void setup() throws Exception {
LocaleContextHolder.setDefaultLocale(Locale.UK);
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean();
validatorBean.setConstraintValidatorFactory(new TestConstraintValidatorFactory(this.testConstraintValidator));
validatorBean.afterPropertiesSet();
this.jakartaValidator = new InvocationCountingValidator(validatorBean);
this.handlerAdapter = initHandlerAdapter(this.jakartaValidator);
@@ -296,6 +314,30 @@ class MethodValidationTests {
arguments []; default message [length must be 10 or under]""");
}
@Test // gh-34105
void typeConstraint() {
this.testConstraintValidator.setReject(true);
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, ""));
this.request.addHeader("header", "12345");
this.request.setContentType("application/json");
this.request.setContent("{\"name\":\"Faustino\"}".getBytes(UTF_8));
HandlerMethodValidationException ex = catchThrowableOfType(HandlerMethodValidationException.class,
() -> this.handlerAdapter.handle(this.request, this.response, hm));
List<ParameterValidationResult> results = ex.getParameterValidationResults();
assertThat(results).hasSize(1);
ParameterValidationResult result = results.get(0);
assertThat(result).isInstanceOf(ParameterErrors.class);
assertBeanResult((Errors) result, "person", List.of("""
Error in object 'person': codes [TestConstraint.person,TestConstraint]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [person]; arguments []; default message []]; default message [Fail message]\
"""
));
}
@SuppressWarnings("unchecked")
private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) {
@@ -306,8 +348,8 @@ class MethodValidationTests {
@SuppressWarnings("SameParameterValue")
private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) {
assertThat(errors.getObjectName()).isEqualTo(objectName);
assertThat(errors.getFieldErrors())
.extracting(FieldError::toString)
assertThat(errors.getAllErrors())
.extracting(ObjectError::toString)
.containsExactlyInAnyOrderElementsOf(fieldErrors);
}
@@ -323,6 +365,7 @@ class MethodValidationTests {
}
@TestConstraint
@SuppressWarnings("unused")
private record Person(@Size(min = 1, max = 10) @JsonProperty("name") String name) {
@@ -356,6 +399,9 @@ class MethodValidationTests {
void handle(@Valid @RequestBody List<Person> persons) {
}
void handle(@Valid @RequestBody Person person, @RequestHeader @Size(min=4) String header) {
}
}
@@ -477,4 +523,57 @@ class MethodValidationTests {
}
}
@Constraint(validatedBy = TestConstraintValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestConstraint {
String message() default "Fail message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
private static class TestConstraintValidator implements ConstraintValidator<TestConstraint, Person> {
private boolean reject;
public void setReject(boolean reject) {
this.reject = reject;
}
@Override
public boolean isValid(Person person, ConstraintValidatorContext context) {
return !this.reject;
}
}
private static class TestConstraintValidatorFactory implements ConstraintValidatorFactory {
private final Map<Class<?>, ConstraintValidator<?, ?>> validators;
private final ConstraintValidatorFactory delegate = new ConstraintValidatorFactoryImpl();
private TestConstraintValidatorFactory(ConstraintValidator<?, ?>... validators) {
this.validators = new LinkedHashMap<>(validators.length);
Arrays.stream(validators).forEach(validator -> this.validators.put(validator.getClass(), validator));
}
@SuppressWarnings("unchecked")
@Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> aClass) {
ConstraintValidator<?, ?> validator = this.validators.get(aClass);
return (validator != null ? (T) validator : this.delegate.getInstance(aClass));
}
@Override
public void releaseInstance(ConstraintValidator<?, ?> constraintValidator) {
}
}
}