From bb7ce210764004d7da8b3d05eb371495d6792db0 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 4 Feb 2025 16:56:44 +0000 Subject: [PATCH 1/4] Expose Locale to method validation in WebFlux Closes gh-33810 --- .../result/method/InvocableHandlerMethod.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 836a7753088..32ae18c6d08 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -39,6 +39,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.SynchronousSink; import reactor.core.scheduler.Scheduler; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.CoroutinesUtils; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.KotlinDetector; @@ -186,8 +187,14 @@ public class InvocableHandlerMethod extends HandlerMethod { return getMethodArgumentValuesOnScheduler(exchange, bindingContext, providedArgs).flatMap(args -> { if (shouldValidateArguments() && this.methodValidator != null) { - this.methodValidator.applyArgumentValidation( - getBean(), getBridgedMethod(), getMethodParameters(), args, this.validationGroups); + try { + LocaleContextHolder.setLocaleContext(exchange.getLocaleContext()); + this.methodValidator.applyArgumentValidation( + getBean(), getBridgedMethod(), getMethodParameters(), args, this.validationGroups); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } } Object value; Method method = getBridgedMethod(); From 3898482d3f494e9bf75e40fc5ae902461c20edb5 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 4 Feb 2025 19:08:52 +0000 Subject: [PATCH 2/4] Revert commit 0f38c28e9155e0190cf3ffb5478aca3e9282b6e7 The fix is not how the issue needs to be addressed. See gh-34121 --- ...vletRequestDataBinderIntegrationTests.java | 3 +++ .../web/bind/ServletRequestDataBinder.java | 4 --- .../bind/ServletRequestDataBinderTests.java | 26 ------------------- 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java index 64d7d5f434b..1750d4a8950 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java @@ -19,6 +19,7 @@ package org.springframework.test.web.servlet.samples.spr; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; @@ -56,6 +57,7 @@ class ServletRequestDataBinderIntegrationTests { .andExpect(content().string("valueB")); } + @Disabled("see gh-34121") @Test // gh-34121 void postArrayWithEmptyIndex(WebApplicationContext wac) throws Exception { MockMvc mockMvc = webAppContextSetup(wac).build(); @@ -86,6 +88,7 @@ class ServletRequestDataBinderIntegrationTests { .andExpect(content().string("valueB")); } + @Disabled("see gh-34121") @Test // gh-34121 void postListWithEmptyIndex(WebApplicationContext wac) throws Exception { MockMvc mockMvc = webAppContextSetup(wac).build(); diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 95ff7db1a33..07ae3555666 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -244,10 +244,6 @@ public class ServletRequestDataBinder extends WebDataBinder { @Nullable protected Object getRequestParameter(String name, Class type) { Object value = this.request.getParameterValues(name); - if (value == null && !name.endsWith("[]") && - (List.class.isAssignableFrom(type) || type.isArray())) { - value = this.request.getParameterValues(name + "[]"); - } return (ObjectUtils.isArray(value) && Array.getLength(value) == 1 ? Array.get(value, 0) : value); } diff --git a/spring-web/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java b/spring-web/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java index fa10f23a045..adbae1ddc62 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java @@ -93,32 +93,6 @@ class ServletRequestDataBinderTests { assertThat(target.isPostProcessed()).isFalse(); } - @Test - public void testFieldWithArrayIndex() { - TestBean target = new TestBean(); - ServletRequestDataBinder binder = new ServletRequestDataBinder(target); - binder.setIgnoreUnknownFields(false); - - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter("stringArray[0]", "ONE"); - request.addParameter("stringArray[1]", "TWO"); - binder.bind(request); - assertThat(target.getStringArray()).containsExactly("ONE", "TWO"); - } - - @Test - public void testFieldWithEmptyArrayIndex() { - TestBean target = new TestBean(); - ServletRequestDataBinder binder = new ServletRequestDataBinder(target); - binder.setIgnoreUnknownFields(false); - - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter("stringArray[]", "ONE"); - request.addParameter("stringArray[]", "TWO"); - binder.bind(request); - assertThat(target.getStringArray()).containsExactly("ONE", "TWO"); - } - @Test void testFieldDefault() { TestBean target = new TestBean(); From 7f29f0e6638591207f903f5366e657395b6d632b Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 4 Feb 2025 19:13:07 +0000 Subject: [PATCH 3/4] Revert commit 3505c4bcad9fab4a6754d6a4f331669722ae2fae The fix did not address the issue. It only made the constructor not fail with tests succeeding due to setter binding instead. See gh-34043 --- .../validation/DataBinder.java | 4 +- ...vletRequestDataBinderIntegrationTests.java | 138 ------------------ 2 files changed, 2 insertions(+), 140 deletions(-) delete mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 086baefad9f..2190ad20fe9 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -1082,7 +1082,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { } int startIdx = paramPath.length() + 1; int endIdx = name.indexOf(']', startIdx); - String nestedPath = ((name.length() > endIdx + 1) ? name.substring(0, endIdx + 2) : ""); + String nestedPath = name.substring(0, endIdx + 2); boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\''); String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx)); if (map == null) { @@ -1116,7 +1116,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { SortedSet indexes = null; for (String name : valueResolver.getNames()) { if (name.startsWith(paramPath + "[")) { - int endIndex = name.indexOf(']', paramPath.length() + 1); + int endIndex = name.indexOf(']', paramPath.length() + 2); String rawIndex = name.substring(paramPath.length() + 1, endIndex); int index = Integer.parseInt(rawIndex); indexes = (indexes != null ? indexes : new TreeSet<>()); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java deleted file mode 100644 index 1750d4a8950..00000000000 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2002-2024 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.test.web.servlet.samples.spr; - -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.WebApplicationContext; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; - -@SpringJUnitWebConfig(ServletRequestDataBinderIntegrationTests.SpringWebKeyValueController.class) -class ServletRequestDataBinderIntegrationTests { - - @Test // gh-34043 - void postMap(WebApplicationContext wac) throws Exception { - MockMvc mockMvc = webAppContextSetup(wac).build(); - mockMvc.perform(post("/map") - .param("someMap[a]", "valueA") - .param("someMap[b]", "valueB")) - .andExpect(status().isOk()) - .andExpect(content().string("valueB")); - } - - @Test - void postArray(WebApplicationContext wac) throws Exception { - MockMvc mockMvc = webAppContextSetup(wac).build(); - mockMvc.perform(post("/array") - .param("someArray[0]", "valueA") - .param("someArray[1]", "valueB")) - .andExpect(status().isOk()) - .andExpect(content().string("valueB")); - } - - @Disabled("see gh-34121") - @Test // gh-34121 - void postArrayWithEmptyIndex(WebApplicationContext wac) throws Exception { - MockMvc mockMvc = webAppContextSetup(wac).build(); - mockMvc.perform(post("/array") - .param("someArray[]", "valueA") - .param("someArray[]", "valueB")) - .andExpect(status().isOk()) - .andExpect(content().string("valueB")); - } - - @Test - void postArrayWithoutIndex(WebApplicationContext wac) throws Exception { - MockMvc mockMvc = webAppContextSetup(wac).build(); - mockMvc.perform(post("/array") - .param("someArray", "valueA") - .param("someArray", "valueB")) - .andExpect(status().isOk()) - .andExpect(content().string("valueB")); - } - - @Test - void postList(WebApplicationContext wac) throws Exception { - MockMvc mockMvc = webAppContextSetup(wac).build(); - mockMvc.perform(post("/list") - .param("someList[0]", "valueA") - .param("someList[1]", "valueB")) - .andExpect(status().isOk()) - .andExpect(content().string("valueB")); - } - - @Disabled("see gh-34121") - @Test // gh-34121 - void postListWithEmptyIndex(WebApplicationContext wac) throws Exception { - MockMvc mockMvc = webAppContextSetup(wac).build(); - mockMvc.perform(post("/list") - .param("someList[]", "valueA") - .param("someList[]", "valueB")) - .andExpect(status().isOk()) - .andExpect(content().string("valueB")); - } - - @Test - void postListWithoutIndex(WebApplicationContext wac) throws Exception { - MockMvc mockMvc = webAppContextSetup(wac).build(); - mockMvc.perform(post("/list") - .param("someList", "valueA") - .param("someList", "valueB")) - .andExpect(status().isOk()) - .andExpect(content().string("valueB")); - } - - record PayloadWithMap(Map someMap) {} - - record PayloadWithArray(String[] someArray) {} - - record PayloadWithList(List someList) {} - - @RestController - @SuppressWarnings("unused") - static class SpringWebKeyValueController { - - @PostMapping("/map") - String postMap(@ModelAttribute("payload") PayloadWithMap payload) { - return payload.someMap.get("b"); - } - - @PostMapping("/array") - String postArray(@ModelAttribute("payload") PayloadWithArray payload) { - return payload.someArray[1]; - } - - @PostMapping("/list") - String postList(@ModelAttribute("payload") PayloadWithList payload) { - return payload.someList.get(1); - } - } - -} From 462c2bd538fea02d56ed6d931866f82feea860d7 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 5 Feb 2025 14:20:05 +0000 Subject: [PATCH 4/4] Enhance constructor binding for List/Map/Array Support List/Map/Array of simple values, or values supported by type conversion. Closes gh-34305 --- .../validation/DataBinder.java | 76 +++++++++++++---- .../validation/DataBinderConstructTests.java | 84 ++++++++++++++----- 2 files changed, 123 insertions(+), 37 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 2190ad20fe9..37cfea19c17 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -964,7 +964,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { value = createMap(paramPath, paramType, resolvableType, valueResolver); } else if (paramType.isArray()) { - value = createArray(paramPath, resolvableType, valueResolver); + value = createArray(paramPath, paramType, resolvableType, valueResolver); } } @@ -981,11 +981,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { } } catch (TypeMismatchException ex) { - ex.initPropertyName(paramPath); args[i] = null; failedParamNames.add(paramPath); - getBindingResult().recordFieldValue(paramPath, paramType, value); - getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult()); + handleTypeMismatchException(ex, paramPath, paramType, value); } } } @@ -1048,9 +1046,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { return false; } - @SuppressWarnings("unchecked") @Nullable - private List createList( + private List createList( String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { ResolvableType elementType = type.getNested(2); @@ -1058,18 +1055,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { if (indexes == null) { return null; } + int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0); - List list = (List) CollectionFactory.createCollection(paramType, size); + List list = (List) CollectionFactory.createCollection(paramType, size); for (int i = 0; i < size; i++) { list.add(null); } + for (int index : indexes) { - list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver)); + String indexedPath = paramPath + "[" + index + "]"; + list.set(index, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver)); } + return list; } - @SuppressWarnings("unchecked") @Nullable private Map createMap( String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { @@ -1080,34 +1080,42 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { if (!name.startsWith(paramPath + "[")) { continue; } + int startIdx = paramPath.length() + 1; int endIdx = name.indexOf(']', startIdx); - String nestedPath = name.substring(0, endIdx + 2); boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\''); String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx)); + if (map == null) { map = CollectionFactory.createMap(paramType, 16); } - if (!map.containsKey(key)) { - map.put(key, (V) createObject(elementType, nestedPath, valueResolver)); - } + + String indexedPath = name.substring(0, endIdx + 1); + map.put(key, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver)); } + return map; } @SuppressWarnings("unchecked") @Nullable - private V[] createArray(String paramPath, ResolvableType type, ValueResolver valueResolver) { + private V[] createArray( + String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { + ResolvableType elementType = type.getNested(2); SortedSet indexes = getIndexes(paramPath, valueResolver); if (indexes == null) { return null; } + int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1: 0); V[] array = (V[]) Array.newInstance(elementType.resolve(), size); + for (int index : indexes) { - array[index] = (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver); + String indexedPath = paramPath + "[" + index + "]"; + array[index] = createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver); } + return array; } @@ -1118,14 +1126,46 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { if (name.startsWith(paramPath + "[")) { int endIndex = name.indexOf(']', paramPath.length() + 2); String rawIndex = name.substring(paramPath.length() + 1, endIndex); - int index = Integer.parseInt(rawIndex); - indexes = (indexes != null ? indexes : new TreeSet<>()); - indexes.add(index); + if (StringUtils.hasLength(rawIndex)) { + int index = Integer.parseInt(rawIndex); + indexes = (indexes != null ? indexes : new TreeSet<>()); + indexes.add(index); + } } } return indexes; } + @SuppressWarnings("unchecked") + private @Nullable V createIndexedValue( + String paramPath, Class paramType, ResolvableType elementType, + String indexedPath, ValueResolver valueResolver) { + + Object value = null; + Class elementClass = elementType.resolve(Object.class); + Object rawValue = valueResolver.resolveValue(indexedPath, elementClass); + if (rawValue != null) { + try { + value = convertIfNecessary(rawValue, elementClass); + } + catch (TypeMismatchException ex) { + handleTypeMismatchException(ex, paramPath, paramType, rawValue); + } + } + else { + value = createObject(elementType, indexedPath + ".", valueResolver); + } + return (V) value; + } + + private void handleTypeMismatchException( + TypeMismatchException ex, String paramPath, Class paramType, @Nullable Object value) { + + ex.initPropertyName(paramPath); + getBindingResult().recordFieldValue(paramPath, paramType, value); + getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult()); + } + private void validateConstructorArgument( Class constructorClass, String nestedPath, String name, @Nullable Object value) { diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java index ccdd1f6b530..7b0fdb72ee5 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java @@ -103,17 +103,17 @@ class DataBinderConstructTests { } @Test - void listBinding() { + void dataClassWithListBinding() { MapValueResolver valueResolver = new MapValueResolver(Map.of( "dataClassList[0].param1", "value1", "dataClassList[0].param2", "true", "dataClassList[1].param1", "value2", "dataClassList[1].param2", "true", "dataClassList[2].param1", "value3", "dataClassList[2].param2", "true")); - DataBinder binder = initDataBinder(ListDataClass.class); + DataBinder binder = initDataBinder(DataClassListRecord.class); binder.construct(valueResolver); - ListDataClass dataClass = getTarget(binder); - List list = dataClass.dataClassList(); + DataClassListRecord target = getTarget(binder); + List list = target.dataClassList(); assertThat(list).hasSize(3); assertThat(list.get(0).param1()).isEqualTo("value1"); @@ -122,17 +122,17 @@ class DataBinderConstructTests { } @Test // gh-34145 - void listBindingWithNonconsecutiveIndices() { + void dataClassWithListBindingWithNonconsecutiveIndices() { 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); + DataBinder binder = initDataBinder(DataClassListRecord.class); binder.construct(valueResolver); - ListDataClass dataClass = getTarget(binder); - List list = dataClass.dataClassList(); + DataClassListRecord target = getTarget(binder); + List list = target.dataClassList(); assertThat(list.get(0).param1()).isEqualTo("value1"); assertThat(list.get(1).param1()).isEqualTo("value2"); @@ -140,17 +140,17 @@ class DataBinderConstructTests { } @Test - void mapBinding() { + void dataClassWithMapBinding() { MapValueResolver valueResolver = new MapValueResolver(Map.of( "dataClassMap[a].param1", "value1", "dataClassMap[a].param2", "true", "dataClassMap[b].param1", "value2", "dataClassMap[b].param2", "true", "dataClassMap['c'].param1", "value3", "dataClassMap['c'].param2", "true")); - DataBinder binder = initDataBinder(MapDataClass.class); + DataBinder binder = initDataBinder(DataClassMapRecord.class); binder.construct(valueResolver); - MapDataClass dataClass = getTarget(binder); - Map map = dataClass.dataClassMap(); + DataClassMapRecord target = getTarget(binder); + Map map = target.dataClassMap(); assertThat(map).hasSize(3); assertThat(map.get("a").param1()).isEqualTo("value1"); @@ -159,17 +159,17 @@ class DataBinderConstructTests { } @Test - void arrayBinding() { + void dataClassWithArrayBinding() { MapValueResolver valueResolver = new MapValueResolver(Map.of( "dataClassArray[0].param1", "value1", "dataClassArray[0].param2", "true", "dataClassArray[1].param1", "value2", "dataClassArray[1].param2", "true", "dataClassArray[2].param1", "value3", "dataClassArray[2].param2", "true")); - DataBinder binder = initDataBinder(ArrayDataClass.class); + DataBinder binder = initDataBinder(DataClassArrayRecord.class); binder.construct(valueResolver); - ArrayDataClass dataClass = getTarget(binder); - DataClass[] array = dataClass.dataClassArray(); + DataClassArrayRecord target = getTarget(binder); + DataClass[] array = target.dataClassArray(); assertThat(array).hasSize(3); assertThat(array[0].param1()).isEqualTo("value1"); @@ -177,6 +177,40 @@ class DataBinderConstructTests { assertThat(array[2].param1()).isEqualTo("value3"); } + @Test + void simpleListBinding() { + MapValueResolver valueResolver = new MapValueResolver(Map.of("integerList[0]", "1", "integerList[1]", "2")); + + DataBinder binder = initDataBinder(IntegerListRecord.class); + binder.construct(valueResolver); + + IntegerListRecord target = getTarget(binder); + assertThat(target.integerList()).containsExactly(1, 2); + } + + @Test + void simpleMapBinding() { + MapValueResolver valueResolver = new MapValueResolver(Map.of("integerMap[a]", "1", "integerMap[b]", "2")); + + DataBinder binder = initDataBinder(IntegerMapRecord.class); + binder.construct(valueResolver); + + IntegerMapRecord target = getTarget(binder); + assertThat(target.integerMap()).hasSize(2).containsEntry("a", 1).containsEntry("b", 2); + } + + @Test + void simpleArrayBinding() { + MapValueResolver valueResolver = new MapValueResolver(Map.of("integerArray[0]", "1", "integerArray[1]", "2")); + + DataBinder binder = initDataBinder(IntegerArrayRecord.class); + binder.construct(valueResolver); + + IntegerArrayRecord target = getTarget(binder); + assertThat(target.integerArray()).containsExactly(1, 2); + } + + @SuppressWarnings("SameParameterValue") private static DataBinder initDataBinder(Class targetType) { DataBinder binder = new DataBinder(null); @@ -248,15 +282,27 @@ class DataBinderConstructTests { } - private record ListDataClass(List dataClassList) { + private record DataClassListRecord(List dataClassList) { + } + + + private record DataClassMapRecord(Map dataClassMap) { + } + + + private record DataClassArrayRecord(DataClass[] dataClassArray) { + } + + + private record IntegerListRecord(List integerList) { } - private record MapDataClass(Map dataClassMap) { + private record IntegerMapRecord(Map integerMap) { } - private record ArrayDataClass(DataClass[] dataClassArray) { + private record IntegerArrayRecord(Integer[] integerArray) { }