Skip to content

Commit 48308ae

Browse files
committed
Merge branch '6.2.x'
2 parents e0521fc + 4350fc2 commit 48308ae

File tree

4 files changed

+124
-5
lines changed

4 files changed

+124
-5
lines changed

spring-context/src/main/java/org/springframework/validation/DataBinder.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,9 @@ private boolean hasValuesFor(String paramPath, ValueResolver resolver) {
10321032
}
10331033
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
10341034
List<V> list = (List<V>) CollectionFactory.createCollection(paramType, size);
1035-
indexes.forEach(i -> list.add(null));
1035+
for (int i = 0; i < size; i++) {
1036+
list.add(null);
1037+
}
10361038
for (int index : indexes) {
10371039
list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver));
10381040
}

spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ else if (arg instanceof Optional<?> optional) {
367367
container = null;
368368
}
369369

370-
if (node.getKind().equals(ElementKind.PROPERTY)) {
370+
if (node.getKind().equals(ElementKind.PROPERTY) || node.getKind().equals(ElementKind.BEAN)) {
371371
nestedViolations
372372
.computeIfAbsent(parameterNode, k ->
373373
new ParamErrorsBuilder(parameter, value, container, index, key))

spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java

+18
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@ void listBinding() {
121121
assertThat(list.get(2).param1()).isEqualTo("value3");
122122
}
123123

124+
@Test // gh-34145
125+
void listBindingWithNonconsecutiveIndices() {
126+
MapValueResolver valueResolver = new MapValueResolver(Map.of(
127+
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
128+
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
129+
"dataClassList[3].param1", "value3", "dataClassList[3].param2", "true"));
130+
131+
DataBinder binder = initDataBinder(ListDataClass.class);
132+
binder.construct(valueResolver);
133+
134+
ListDataClass dataClass = getTarget(binder);
135+
List<DataClass> list = dataClass.dataClassList();
136+
137+
assertThat(list.get(0).param1()).isEqualTo("value1");
138+
assertThat(list.get(1).param1()).isEqualTo("value2");
139+
assertThat(list.get(3).param1()).isEqualTo("value3");
140+
}
141+
124142
@Test
125143
void mapBinding() {
126144
MapValueResolver valueResolver = new MapValueResolver(Map.of(

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java

+102-3
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,32 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
1923
import java.lang.reflect.Method;
24+
import java.util.Arrays;
2025
import java.util.HashMap;
26+
import java.util.LinkedHashMap;
2127
import java.util.List;
2228
import java.util.Locale;
29+
import java.util.Map;
2330
import java.util.Set;
2431
import java.util.function.Consumer;
2532

2633
import com.fasterxml.jackson.annotation.JsonProperty;
34+
import jakarta.validation.Constraint;
35+
import jakarta.validation.ConstraintValidator;
36+
import jakarta.validation.ConstraintValidatorContext;
37+
import jakarta.validation.ConstraintValidatorFactory;
2738
import jakarta.validation.ConstraintViolation;
39+
import jakarta.validation.Payload;
2840
import jakarta.validation.Valid;
2941
import jakarta.validation.constraints.Size;
3042
import jakarta.validation.executable.ExecutableValidator;
3143
import jakarta.validation.metadata.BeanDescriptor;
44+
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl;
3245
import org.junit.jupiter.api.AfterEach;
3346
import org.junit.jupiter.api.BeforeEach;
3447
import org.junit.jupiter.api.Test;
@@ -39,11 +52,12 @@
3952
import org.springframework.http.converter.StringHttpMessageConverter;
4053
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
4154
import org.springframework.validation.Errors;
42-
import org.springframework.validation.FieldError;
55+
import org.springframework.validation.ObjectError;
4356
import org.springframework.validation.Validator;
4457
import org.springframework.validation.annotation.Validated;
4558
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
4659
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
60+
import org.springframework.validation.method.ParameterErrors;
4761
import org.springframework.validation.method.ParameterValidationResult;
4862
import org.springframework.web.bind.MethodArgumentNotValidException;
4963
import org.springframework.web.bind.WebDataBinder;
@@ -91,13 +105,17 @@ class MethodValidationTests {
91105

92106
private InvocationCountingValidator jakartaValidator;
93107

108+
private final TestConstraintValidator testConstraintValidator = new TestConstraintValidator();
109+
94110

95111
@BeforeEach
96112
void setup() throws Exception {
97113
LocaleContextHolder.setDefaultLocale(Locale.UK);
98114

99115
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean();
116+
validatorBean.setConstraintValidatorFactory(new TestConstraintValidatorFactory(this.testConstraintValidator));
100117
validatorBean.afterPropertiesSet();
118+
101119
this.jakartaValidator = new InvocationCountingValidator(validatorBean);
102120

103121
this.handlerAdapter = initHandlerAdapter(this.jakartaValidator);
@@ -296,6 +314,30 @@ void springValidator() throws Exception {
296314
arguments []; default message [length must be 10 or under]""");
297315
}
298316

317+
@Test // gh-34105
318+
void typeConstraint() {
319+
this.testConstraintValidator.setReject(true);
320+
321+
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, ""));
322+
this.request.addHeader("header", "12345");
323+
this.request.setContentType("application/json");
324+
this.request.setContent("{\"name\":\"Faustino\"}".getBytes(UTF_8));
325+
326+
HandlerMethodValidationException ex = catchThrowableOfType(HandlerMethodValidationException.class,
327+
() -> this.handlerAdapter.handle(this.request, this.response, hm));
328+
329+
List<ParameterValidationResult> results = ex.getParameterValidationResults();
330+
assertThat(results).hasSize(1);
331+
ParameterValidationResult result = results.get(0);
332+
assertThat(result).isInstanceOf(ParameterErrors.class);
333+
334+
assertBeanResult((Errors) result, "person", List.of("""
335+
Error in object 'person': codes [TestConstraint.person,TestConstraint]; \
336+
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
337+
codes [person]; arguments []; default message []]; default message [Fail message]\
338+
"""
339+
));
340+
}
299341

300342
@SuppressWarnings("unchecked")
301343
private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) {
@@ -306,8 +348,8 @@ private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCal
306348
@SuppressWarnings("SameParameterValue")
307349
private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) {
308350
assertThat(errors.getObjectName()).isEqualTo(objectName);
309-
assertThat(errors.getFieldErrors())
310-
.extracting(FieldError::toString)
351+
assertThat(errors.getAllErrors())
352+
.extracting(ObjectError::toString)
311353
.containsExactlyInAnyOrderElementsOf(fieldErrors);
312354
}
313355

@@ -323,6 +365,7 @@ private static void assertValueResult(
323365
}
324366

325367

368+
@TestConstraint
326369
@SuppressWarnings("unused")
327370
private record Person(@Size(min = 1, max = 10) @JsonProperty("name") String name) {
328371

@@ -356,6 +399,9 @@ String handleValidated(@Validated Person person, Errors errors,
356399

357400
void handle(@Valid @RequestBody List<Person> persons) {
358401
}
402+
403+
void handle(@Valid @RequestBody Person person, @RequestHeader @Size(min=4) String header) {
404+
}
359405
}
360406

361407

@@ -477,4 +523,57 @@ private void assertCountAndIncrement() {
477523
}
478524
}
479525

526+
527+
528+
@Constraint(validatedBy = TestConstraintValidator.class)
529+
@Target({ElementType.TYPE})
530+
@Retention(RetentionPolicy.RUNTIME)
531+
public @interface TestConstraint {
532+
533+
String message() default "Fail message";
534+
535+
Class<?>[] groups() default {};
536+
537+
Class<? extends Payload>[] payload() default {};
538+
}
539+
540+
541+
private static class TestConstraintValidator implements ConstraintValidator<TestConstraint, Person> {
542+
543+
private boolean reject;
544+
545+
public void setReject(boolean reject) {
546+
this.reject = reject;
547+
}
548+
549+
@Override
550+
public boolean isValid(Person person, ConstraintValidatorContext context) {
551+
return !this.reject;
552+
}
553+
}
554+
555+
556+
private static class TestConstraintValidatorFactory implements ConstraintValidatorFactory {
557+
558+
private final Map<Class<?>, ConstraintValidator<?, ?>> validators;
559+
560+
private final ConstraintValidatorFactory delegate = new ConstraintValidatorFactoryImpl();
561+
562+
private TestConstraintValidatorFactory(ConstraintValidator<?, ?>... validators) {
563+
this.validators = new LinkedHashMap<>(validators.length);
564+
Arrays.stream(validators).forEach(validator -> this.validators.put(validator.getClass(), validator));
565+
}
566+
567+
@SuppressWarnings("unchecked")
568+
@Override
569+
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> aClass) {
570+
ConstraintValidator<?, ?> validator = this.validators.get(aClass);
571+
return (validator != null ? (T) validator : this.delegate.getInstance(aClass));
572+
}
573+
574+
@Override
575+
public void releaseInstance(ConstraintValidator<?, ?> constraintValidator) {
576+
}
577+
}
578+
480579
}

0 commit comments

Comments
 (0)