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 30616e5831..6d3c9fbbe0 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -142,6 +142,10 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { */ protected static final Log logger = LogFactory.getLog(DataBinder.class); + /** Internal constant for constructor binding via "[]". */ + private static final int NO_INDEX = -1; + + private @Nullable Object target; @Nullable ResolvableType targetType; @@ -1028,15 +1032,17 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { return null; } - int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0); + int lastIndex = Math.max(indexes.last(), 0); + int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1 : 0); List list = (List) CollectionFactory.createCollection(paramType, size); for (int i = 0; i < size; i++) { list.add(null); } for (int index : indexes) { - String indexedPath = paramPath + "[" + index + "]"; - list.set(index, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver)); + String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]"; + list.set(Math.max(index, 0), + createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver)); } return list; @@ -1078,12 +1084,14 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { return null; } - int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1: 0); + int lastIndex = Math.max(indexes.last(), 0); + int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1: 0); @Nullable V[] array = (V[]) Array.newInstance(elementType.resolve(), size); for (int index : indexes) { - String indexedPath = paramPath + "[" + index + "]"; - array[index] = createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver); + String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]"; + array[Math.max(index, 0)] = + createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver); } return array; @@ -1093,13 +1101,20 @@ 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); - String rawIndex = name.substring(paramPath.length() + 1, endIndex); - if (StringUtils.hasLength(rawIndex)) { - int index = Integer.parseInt(rawIndex); - indexes = (indexes != null ? indexes : new TreeSet<>()); - indexes.add(index); + int index; + if (paramPath.length() + 2 == name.length()) { + if (!name.endsWith("[]")) { + continue; + } + index = NO_INDEX; } + else { + int endIndex = name.indexOf(']', paramPath.length() + 2); + String indexValue = name.substring(paramPath.length() + 1, endIndex); + index = Integer.parseInt(indexValue); + } + indexes = (indexes != null ? indexes : new TreeSet<>()); + indexes.add(index); } } return indexes; @@ -1107,23 +1122,36 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @SuppressWarnings("unchecked") private @Nullable V createIndexedValue( - String paramPath, Class paramType, ResolvableType elementType, + String paramPath, Class containerType, 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); - } + + if (List.class.isAssignableFrom(elementClass)) { + value = createList(indexedPath, elementClass, elementType, valueResolver); + } + else if (Map.class.isAssignableFrom(elementClass)) { + value = createMap(indexedPath, elementClass, elementType, valueResolver); + } + else if (elementClass.isArray()) { + value = createArray(indexedPath, elementClass, elementType, valueResolver); } else { - value = createObject(elementType, indexedPath + ".", valueResolver); + Object rawValue = valueResolver.resolveValue(indexedPath, elementClass); + if (rawValue != null) { + try { + value = convertIfNecessary(rawValue, elementClass); + } + catch (TypeMismatchException ex) { + handleTypeMismatchException(ex, paramPath, containerType, rawValue); + } + } + else { + value = createObject(elementType, indexedPath + ".", valueResolver); + } } + return (V) 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 83f81a4d4a..c61792783d 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java @@ -188,6 +188,17 @@ class DataBinderConstructTests { assertThat(target.integerList()).containsExactly(1, 2); } + @Test + void simpleListBindingEmptyBrackets() { + MapValueResolver valueResolver = new MapValueResolver(Map.of("integerList[]", "1")); + + DataBinder binder = initDataBinder(IntegerListRecord.class); + binder.construct(valueResolver); + + IntegerListRecord target = getTarget(binder); + assertThat(target.integerList()).containsExactly(1); + } + @Test void simpleMapBinding() { MapValueResolver valueResolver = new MapValueResolver(Map.of("integerMap[a]", "1", "integerMap[b]", "2")); @@ -210,6 +221,34 @@ class DataBinderConstructTests { assertThat(target.integerArray()).containsExactly(1, 2); } + @Test + void nestedListWithinMap() { + MapValueResolver valueResolver = new MapValueResolver(Map.of( + "integerListMap[a][0]", "1", "integerListMap[a][1]", "2", + "integerListMap[b][0]", "3", "integerListMap[b][1]", "4")); + + DataBinder binder = initDataBinder(IntegerListMapRecord.class); + binder.construct(valueResolver); + + IntegerListMapRecord target = getTarget(binder); + assertThat(target.integerListMap().get("a")).containsExactly(1, 2); + assertThat(target.integerListMap().get("b")).containsExactly(3, 4); + } + + @Test + void nestedMapWithinList() { + MapValueResolver valueResolver = new MapValueResolver(Map.of( + "integerMapList[0][a]", "1", "integerMapList[0][b]", "2", + "integerMapList[1][a]", "3", "integerMapList[1][b]", "4")); + + DataBinder binder = initDataBinder(IntegerMapListRecord.class); + binder.construct(valueResolver); + + IntegerMapListRecord target = getTarget(binder); + assertThat(target.integerMapList().get(0)).containsOnly(Map.entry("a", 1), Map.entry("b", 2)); + assertThat(target.integerMapList().get(1)).containsOnly(Map.entry("a", 3), Map.entry("b", 4)); + } + @SuppressWarnings("SameParameterValue") private static DataBinder initDataBinder(Class targetType) { @@ -304,6 +343,14 @@ class DataBinderConstructTests { } + private record IntegerMapListRecord(List> integerMapList) { + } + + + private record IntegerListMapRecord(Map> integerListMap) { + } + + private record MapValueResolver(Map map) implements DataBinder.ValueResolver { @Override diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index 370fd84b60..0a7e4975ce 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -465,7 +465,7 @@ public class HttpRequestValues { UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(uriTemplate); for (Map.Entry> entry : requestParams.entrySet()) { - String nameVar = entry.getKey(); + String nameVar = entry.getKey().replace(":", "%3A"); // suppress treatment as regex uriVars.put(nameVar, entry.getKey()); for (int j = 0; j < entry.getValue().size(); j++) { String valueVar = nameVar + "[" + j + "]"; diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java index f929e0c72b..5f026afe65 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java @@ -99,6 +99,24 @@ class HttpRequestValuesTests { .isEqualTo("/path?param1=1st%20value¶m2=2nd%20value%20A¶m2=2nd%20value%20B"); } + @Test // gh-34364 + void queryParamWithSemicolon() { + HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.POST) + .setUriTemplate("/path") + .addRequestParameter("userId:eq", "test value") + .build(); + + String uriTemplate = requestValues.getUriTemplate(); + assertThat(uriTemplate).isEqualTo("/path?{userId%3Aeq}={userId%3Aeq[0]}"); + + URI uri = UriComponentsBuilder.fromUriString(uriTemplate) + .encode() + .build(requestValues.getUriVariables()); + + assertThat(uri.toString()) + .isEqualTo("/path?userId%3Aeq=test%20value"); + } + @Test void queryParamsWithPreparedUri() {