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 41b8ec3a58..3e9c9893ed 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -1080,7 +1080,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { } int startIdx = paramPath.length() + 1; int endIdx = name.indexOf(']', startIdx); - String nestedPath = name.substring(0, endIdx + 2); + String nestedPath = ((name.length() > endIdx + 1) ? 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) { @@ -1114,7 +1114,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() + 2); + int endIndex = name.indexOf(']', paramPath.length() + 1); 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 new file mode 100644 index 0000000000..1750d4a895 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/ServletRequestDataBinderIntegrationTests.java @@ -0,0 +1,138 @@ +/* + * 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); + } + } + +}