Skip to content

Commit bae12e7

Browse files
committed
Merge branch '6.2.x'
2 parents 85855ec + d04883f commit bae12e7

File tree

4 files changed

+116
-23
lines changed

4 files changed

+116
-23
lines changed

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

+50-22
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
142142
*/
143143
protected static final Log logger = LogFactory.getLog(DataBinder.class);
144144

145+
/** Internal constant for constructor binding via "[]". */
146+
private static final int NO_INDEX = -1;
147+
148+
145149
private @Nullable Object target;
146150

147151
@Nullable ResolvableType targetType;
@@ -1028,15 +1032,17 @@ private boolean hasValuesFor(String paramPath, ValueResolver resolver) {
10281032
return null;
10291033
}
10301034

1031-
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
1035+
int lastIndex = Math.max(indexes.last(), 0);
1036+
int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1 : 0);
10321037
List<?> list = (List<?>) CollectionFactory.createCollection(paramType, size);
10331038
for (int i = 0; i < size; i++) {
10341039
list.add(null);
10351040
}
10361041

10371042
for (int index : indexes) {
1038-
String indexedPath = paramPath + "[" + index + "]";
1039-
list.set(index, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
1043+
String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]";
1044+
list.set(Math.max(index, 0),
1045+
createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
10401046
}
10411047

10421048
return list;
@@ -1078,12 +1084,14 @@ private boolean hasValuesFor(String paramPath, ValueResolver resolver) {
10781084
return null;
10791085
}
10801086

1081-
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1: 0);
1087+
int lastIndex = Math.max(indexes.last(), 0);
1088+
int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1: 0);
10821089
@Nullable V[] array = (V[]) Array.newInstance(elementType.resolve(), size);
10831090

10841091
for (int index : indexes) {
1085-
String indexedPath = paramPath + "[" + index + "]";
1086-
array[index] = createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver);
1092+
String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]";
1093+
array[Math.max(index, 0)] =
1094+
createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver);
10871095
}
10881096

10891097
return array;
@@ -1093,37 +1101,57 @@ private boolean hasValuesFor(String paramPath, ValueResolver resolver) {
10931101
SortedSet<Integer> indexes = null;
10941102
for (String name : valueResolver.getNames()) {
10951103
if (name.startsWith(paramPath + "[")) {
1096-
int endIndex = name.indexOf(']', paramPath.length() + 2);
1097-
String rawIndex = name.substring(paramPath.length() + 1, endIndex);
1098-
if (StringUtils.hasLength(rawIndex)) {
1099-
int index = Integer.parseInt(rawIndex);
1100-
indexes = (indexes != null ? indexes : new TreeSet<>());
1101-
indexes.add(index);
1104+
int index;
1105+
if (paramPath.length() + 2 == name.length()) {
1106+
if (!name.endsWith("[]")) {
1107+
continue;
1108+
}
1109+
index = NO_INDEX;
1110+
}
1111+
else {
1112+
int endIndex = name.indexOf(']', paramPath.length() + 2);
1113+
String indexValue = name.substring(paramPath.length() + 1, endIndex);
1114+
index = Integer.parseInt(indexValue);
11021115
}
1116+
indexes = (indexes != null ? indexes : new TreeSet<>());
1117+
indexes.add(index);
11031118
}
11041119
}
11051120
return indexes;
11061121
}
11071122

11081123
@SuppressWarnings("unchecked")
11091124
private <V> @Nullable V createIndexedValue(
1110-
String paramPath, Class<?> paramType, ResolvableType elementType,
1125+
String paramPath, Class<?> containerType, ResolvableType elementType,
11111126
String indexedPath, ValueResolver valueResolver) {
11121127

11131128
Object value = null;
11141129
Class<?> elementClass = elementType.resolve(Object.class);
1115-
Object rawValue = valueResolver.resolveValue(indexedPath, elementClass);
1116-
if (rawValue != null) {
1117-
try {
1118-
value = convertIfNecessary(rawValue, elementClass);
1119-
}
1120-
catch (TypeMismatchException ex) {
1121-
handleTypeMismatchException(ex, paramPath, paramType, rawValue);
1122-
}
1130+
1131+
if (List.class.isAssignableFrom(elementClass)) {
1132+
value = createList(indexedPath, elementClass, elementType, valueResolver);
1133+
}
1134+
else if (Map.class.isAssignableFrom(elementClass)) {
1135+
value = createMap(indexedPath, elementClass, elementType, valueResolver);
1136+
}
1137+
else if (elementClass.isArray()) {
1138+
value = createArray(indexedPath, elementClass, elementType, valueResolver);
11231139
}
11241140
else {
1125-
value = createObject(elementType, indexedPath + ".", valueResolver);
1141+
Object rawValue = valueResolver.resolveValue(indexedPath, elementClass);
1142+
if (rawValue != null) {
1143+
try {
1144+
value = convertIfNecessary(rawValue, elementClass);
1145+
}
1146+
catch (TypeMismatchException ex) {
1147+
handleTypeMismatchException(ex, paramPath, containerType, rawValue);
1148+
}
1149+
}
1150+
else {
1151+
value = createObject(elementType, indexedPath + ".", valueResolver);
1152+
}
11261153
}
1154+
11271155
return (V) value;
11281156
}
11291157

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

+47
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,17 @@ void simpleListBinding() {
188188
assertThat(target.integerList()).containsExactly(1, 2);
189189
}
190190

191+
@Test
192+
void simpleListBindingEmptyBrackets() {
193+
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerList[]", "1"));
194+
195+
DataBinder binder = initDataBinder(IntegerListRecord.class);
196+
binder.construct(valueResolver);
197+
198+
IntegerListRecord target = getTarget(binder);
199+
assertThat(target.integerList()).containsExactly(1);
200+
}
201+
191202
@Test
192203
void simpleMapBinding() {
193204
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerMap[a]", "1", "integerMap[b]", "2"));
@@ -210,6 +221,34 @@ void simpleArrayBinding() {
210221
assertThat(target.integerArray()).containsExactly(1, 2);
211222
}
212223

224+
@Test
225+
void nestedListWithinMap() {
226+
MapValueResolver valueResolver = new MapValueResolver(Map.of(
227+
"integerListMap[a][0]", "1", "integerListMap[a][1]", "2",
228+
"integerListMap[b][0]", "3", "integerListMap[b][1]", "4"));
229+
230+
DataBinder binder = initDataBinder(IntegerListMapRecord.class);
231+
binder.construct(valueResolver);
232+
233+
IntegerListMapRecord target = getTarget(binder);
234+
assertThat(target.integerListMap().get("a")).containsExactly(1, 2);
235+
assertThat(target.integerListMap().get("b")).containsExactly(3, 4);
236+
}
237+
238+
@Test
239+
void nestedMapWithinList() {
240+
MapValueResolver valueResolver = new MapValueResolver(Map.of(
241+
"integerMapList[0][a]", "1", "integerMapList[0][b]", "2",
242+
"integerMapList[1][a]", "3", "integerMapList[1][b]", "4"));
243+
244+
DataBinder binder = initDataBinder(IntegerMapListRecord.class);
245+
binder.construct(valueResolver);
246+
247+
IntegerMapListRecord target = getTarget(binder);
248+
assertThat(target.integerMapList().get(0)).containsOnly(Map.entry("a", 1), Map.entry("b", 2));
249+
assertThat(target.integerMapList().get(1)).containsOnly(Map.entry("a", 3), Map.entry("b", 4));
250+
}
251+
213252

214253
@SuppressWarnings("SameParameterValue")
215254
private static DataBinder initDataBinder(Class<?> targetType) {
@@ -304,6 +343,14 @@ private record IntegerArrayRecord(Integer[] integerArray) {
304343
}
305344

306345

346+
private record IntegerMapListRecord(List<Map<String, Integer>> integerMapList) {
347+
}
348+
349+
350+
private record IntegerListMapRecord(Map<String, List<Integer>> integerListMap) {
351+
}
352+
353+
307354
private record MapValueResolver(Map<String, Object> map) implements DataBinder.ValueResolver {
308355

309356
@Override

spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ private String appendQueryParams(
465465

466466
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(uriTemplate);
467467
for (Map.Entry<String, List<String>> entry : requestParams.entrySet()) {
468-
String nameVar = entry.getKey();
468+
String nameVar = entry.getKey().replace(":", "%3A"); // suppress treatment as regex
469469
uriVars.put(nameVar, entry.getKey());
470470
for (int j = 0; j < entry.getValue().size(); j++) {
471471
String valueVar = nameVar + "[" + j + "]";

spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java

+18
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ void queryParamsWithUriTemplate() {
9999
.isEqualTo("/path?param1=1st%20value&param2=2nd%20value%20A&param2=2nd%20value%20B");
100100
}
101101

102+
@Test // gh-34364
103+
void queryParamWithSemicolon() {
104+
HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.POST)
105+
.setUriTemplate("/path")
106+
.addRequestParameter("userId:eq", "test value")
107+
.build();
108+
109+
String uriTemplate = requestValues.getUriTemplate();
110+
assertThat(uriTemplate).isEqualTo("/path?{userId%3Aeq}={userId%3Aeq[0]}");
111+
112+
URI uri = UriComponentsBuilder.fromUriString(uriTemplate)
113+
.encode()
114+
.build(requestValues.getUriVariables());
115+
116+
assertThat(uri.toString())
117+
.isEqualTo("/path?userId%3Aeq=test%20value");
118+
}
119+
102120
@Test
103121
void queryParamsWithPreparedUri() {
104122

0 commit comments

Comments
 (0)