Skip to content

Commit 462c2bd

Browse files
committed
Enhance constructor binding for List/Map/Array
Support List/Map/Array of simple values, or values supported by type conversion. Closes gh-34305
1 parent 7f29f0e commit 462c2bd

File tree

2 files changed

+123
-37
lines changed

2 files changed

+123
-37
lines changed

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

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ else if (Map.class.isAssignableFrom(paramType)) {
964964
value = createMap(paramPath, paramType, resolvableType, valueResolver);
965965
}
966966
else if (paramType.isArray()) {
967-
value = createArray(paramPath, resolvableType, valueResolver);
967+
value = createArray(paramPath, paramType, resolvableType, valueResolver);
968968
}
969969
}
970970

@@ -981,11 +981,9 @@ else if (paramType.isArray()) {
981981
}
982982
}
983983
catch (TypeMismatchException ex) {
984-
ex.initPropertyName(paramPath);
985984
args[i] = null;
986985
failedParamNames.add(paramPath);
987-
getBindingResult().recordFieldValue(paramPath, paramType, value);
988-
getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult());
986+
handleTypeMismatchException(ex, paramPath, paramType, value);
989987
}
990988
}
991989
}
@@ -1048,28 +1046,30 @@ private boolean hasValuesFor(String paramPath, ValueResolver resolver) {
10481046
return false;
10491047
}
10501048

1051-
@SuppressWarnings("unchecked")
10521049
@Nullable
1053-
private <V> List<V> createList(
1050+
private List<?> createList(
10541051
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
10551052

10561053
ResolvableType elementType = type.getNested(2);
10571054
SortedSet<Integer> indexes = getIndexes(paramPath, valueResolver);
10581055
if (indexes == null) {
10591056
return null;
10601057
}
1058+
10611059
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
1062-
List<V> list = (List<V>) CollectionFactory.createCollection(paramType, size);
1060+
List<?> list = (List<?>) CollectionFactory.createCollection(paramType, size);
10631061
for (int i = 0; i < size; i++) {
10641062
list.add(null);
10651063
}
1064+
10661065
for (int index : indexes) {
1067-
list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver));
1066+
String indexedPath = paramPath + "[" + index + "]";
1067+
list.set(index, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
10681068
}
1069+
10691070
return list;
10701071
}
10711072

1072-
@SuppressWarnings("unchecked")
10731073
@Nullable
10741074
private <V> Map<String, V> createMap(
10751075
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
@@ -1080,34 +1080,42 @@ private <V> Map<String, V> createMap(
10801080
if (!name.startsWith(paramPath + "[")) {
10811081
continue;
10821082
}
1083+
10831084
int startIdx = paramPath.length() + 1;
10841085
int endIdx = name.indexOf(']', startIdx);
1085-
String nestedPath = name.substring(0, endIdx + 2);
10861086
boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\'');
10871087
String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx));
1088+
10881089
if (map == null) {
10891090
map = CollectionFactory.createMap(paramType, 16);
10901091
}
1091-
if (!map.containsKey(key)) {
1092-
map.put(key, (V) createObject(elementType, nestedPath, valueResolver));
1093-
}
1092+
1093+
String indexedPath = name.substring(0, endIdx + 1);
1094+
map.put(key, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
10941095
}
1096+
10951097
return map;
10961098
}
10971099

10981100
@SuppressWarnings("unchecked")
10991101
@Nullable
1100-
private <V> V[] createArray(String paramPath, ResolvableType type, ValueResolver valueResolver) {
1102+
private <V> V[] createArray(
1103+
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
1104+
11011105
ResolvableType elementType = type.getNested(2);
11021106
SortedSet<Integer> indexes = getIndexes(paramPath, valueResolver);
11031107
if (indexes == null) {
11041108
return null;
11051109
}
1110+
11061111
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1: 0);
11071112
V[] array = (V[]) Array.newInstance(elementType.resolve(), size);
1113+
11081114
for (int index : indexes) {
1109-
array[index] = (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver);
1115+
String indexedPath = paramPath + "[" + index + "]";
1116+
array[index] = createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver);
11101117
}
1118+
11111119
return array;
11121120
}
11131121

@@ -1118,14 +1126,46 @@ private static SortedSet<Integer> getIndexes(String paramPath, ValueResolver val
11181126
if (name.startsWith(paramPath + "[")) {
11191127
int endIndex = name.indexOf(']', paramPath.length() + 2);
11201128
String rawIndex = name.substring(paramPath.length() + 1, endIndex);
1121-
int index = Integer.parseInt(rawIndex);
1122-
indexes = (indexes != null ? indexes : new TreeSet<>());
1123-
indexes.add(index);
1129+
if (StringUtils.hasLength(rawIndex)) {
1130+
int index = Integer.parseInt(rawIndex);
1131+
indexes = (indexes != null ? indexes : new TreeSet<>());
1132+
indexes.add(index);
1133+
}
11241134
}
11251135
}
11261136
return indexes;
11271137
}
11281138

1139+
@SuppressWarnings("unchecked")
1140+
private <V> @Nullable V createIndexedValue(
1141+
String paramPath, Class<?> paramType, ResolvableType elementType,
1142+
String indexedPath, ValueResolver valueResolver) {
1143+
1144+
Object value = null;
1145+
Class<?> elementClass = elementType.resolve(Object.class);
1146+
Object rawValue = valueResolver.resolveValue(indexedPath, elementClass);
1147+
if (rawValue != null) {
1148+
try {
1149+
value = convertIfNecessary(rawValue, elementClass);
1150+
}
1151+
catch (TypeMismatchException ex) {
1152+
handleTypeMismatchException(ex, paramPath, paramType, rawValue);
1153+
}
1154+
}
1155+
else {
1156+
value = createObject(elementType, indexedPath + ".", valueResolver);
1157+
}
1158+
return (V) value;
1159+
}
1160+
1161+
private void handleTypeMismatchException(
1162+
TypeMismatchException ex, String paramPath, Class<?> paramType, @Nullable Object value) {
1163+
1164+
ex.initPropertyName(paramPath);
1165+
getBindingResult().recordFieldValue(paramPath, paramType, value);
1166+
getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult());
1167+
}
1168+
11291169
private void validateConstructorArgument(
11301170
Class<?> constructorClass, String nestedPath, String name, @Nullable Object value) {
11311171

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

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,17 @@ void dataClassBindingWithConversionError() {
103103
}
104104

105105
@Test
106-
void listBinding() {
106+
void dataClassWithListBinding() {
107107
MapValueResolver valueResolver = new MapValueResolver(Map.of(
108108
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
109109
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
110110
"dataClassList[2].param1", "value3", "dataClassList[2].param2", "true"));
111111

112-
DataBinder binder = initDataBinder(ListDataClass.class);
112+
DataBinder binder = initDataBinder(DataClassListRecord.class);
113113
binder.construct(valueResolver);
114114

115-
ListDataClass dataClass = getTarget(binder);
116-
List<DataClass> list = dataClass.dataClassList();
115+
DataClassListRecord target = getTarget(binder);
116+
List<DataClass> list = target.dataClassList();
117117

118118
assertThat(list).hasSize(3);
119119
assertThat(list.get(0).param1()).isEqualTo("value1");
@@ -122,35 +122,35 @@ void listBinding() {
122122
}
123123

124124
@Test // gh-34145
125-
void listBindingWithNonconsecutiveIndices() {
125+
void dataClassWithListBindingWithNonconsecutiveIndices() {
126126
MapValueResolver valueResolver = new MapValueResolver(Map.of(
127127
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
128128
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
129129
"dataClassList[3].param1", "value3", "dataClassList[3].param2", "true"));
130130

131-
DataBinder binder = initDataBinder(ListDataClass.class);
131+
DataBinder binder = initDataBinder(DataClassListRecord.class);
132132
binder.construct(valueResolver);
133133

134-
ListDataClass dataClass = getTarget(binder);
135-
List<DataClass> list = dataClass.dataClassList();
134+
DataClassListRecord target = getTarget(binder);
135+
List<DataClass> list = target.dataClassList();
136136

137137
assertThat(list.get(0).param1()).isEqualTo("value1");
138138
assertThat(list.get(1).param1()).isEqualTo("value2");
139139
assertThat(list.get(3).param1()).isEqualTo("value3");
140140
}
141141

142142
@Test
143-
void mapBinding() {
143+
void dataClassWithMapBinding() {
144144
MapValueResolver valueResolver = new MapValueResolver(Map.of(
145145
"dataClassMap[a].param1", "value1", "dataClassMap[a].param2", "true",
146146
"dataClassMap[b].param1", "value2", "dataClassMap[b].param2", "true",
147147
"dataClassMap['c'].param1", "value3", "dataClassMap['c'].param2", "true"));
148148

149-
DataBinder binder = initDataBinder(MapDataClass.class);
149+
DataBinder binder = initDataBinder(DataClassMapRecord.class);
150150
binder.construct(valueResolver);
151151

152-
MapDataClass dataClass = getTarget(binder);
153-
Map<String, DataClass> map = dataClass.dataClassMap();
152+
DataClassMapRecord target = getTarget(binder);
153+
Map<String, DataClass> map = target.dataClassMap();
154154

155155
assertThat(map).hasSize(3);
156156
assertThat(map.get("a").param1()).isEqualTo("value1");
@@ -159,24 +159,58 @@ void mapBinding() {
159159
}
160160

161161
@Test
162-
void arrayBinding() {
162+
void dataClassWithArrayBinding() {
163163
MapValueResolver valueResolver = new MapValueResolver(Map.of(
164164
"dataClassArray[0].param1", "value1", "dataClassArray[0].param2", "true",
165165
"dataClassArray[1].param1", "value2", "dataClassArray[1].param2", "true",
166166
"dataClassArray[2].param1", "value3", "dataClassArray[2].param2", "true"));
167167

168-
DataBinder binder = initDataBinder(ArrayDataClass.class);
168+
DataBinder binder = initDataBinder(DataClassArrayRecord.class);
169169
binder.construct(valueResolver);
170170

171-
ArrayDataClass dataClass = getTarget(binder);
172-
DataClass[] array = dataClass.dataClassArray();
171+
DataClassArrayRecord target = getTarget(binder);
172+
DataClass[] array = target.dataClassArray();
173173

174174
assertThat(array).hasSize(3);
175175
assertThat(array[0].param1()).isEqualTo("value1");
176176
assertThat(array[1].param1()).isEqualTo("value2");
177177
assertThat(array[2].param1()).isEqualTo("value3");
178178
}
179179

180+
@Test
181+
void simpleListBinding() {
182+
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerList[0]", "1", "integerList[1]", "2"));
183+
184+
DataBinder binder = initDataBinder(IntegerListRecord.class);
185+
binder.construct(valueResolver);
186+
187+
IntegerListRecord target = getTarget(binder);
188+
assertThat(target.integerList()).containsExactly(1, 2);
189+
}
190+
191+
@Test
192+
void simpleMapBinding() {
193+
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerMap[a]", "1", "integerMap[b]", "2"));
194+
195+
DataBinder binder = initDataBinder(IntegerMapRecord.class);
196+
binder.construct(valueResolver);
197+
198+
IntegerMapRecord target = getTarget(binder);
199+
assertThat(target.integerMap()).hasSize(2).containsEntry("a", 1).containsEntry("b", 2);
200+
}
201+
202+
@Test
203+
void simpleArrayBinding() {
204+
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerArray[0]", "1", "integerArray[1]", "2"));
205+
206+
DataBinder binder = initDataBinder(IntegerArrayRecord.class);
207+
binder.construct(valueResolver);
208+
209+
IntegerArrayRecord target = getTarget(binder);
210+
assertThat(target.integerArray()).containsExactly(1, 2);
211+
}
212+
213+
180214
@SuppressWarnings("SameParameterValue")
181215
private static DataBinder initDataBinder(Class<?> targetType) {
182216
DataBinder binder = new DataBinder(null);
@@ -248,15 +282,27 @@ public DataClass nestedParam2() {
248282
}
249283

250284

251-
private record ListDataClass(List<DataClass> dataClassList) {
285+
private record DataClassListRecord(List<DataClass> dataClassList) {
286+
}
287+
288+
289+
private record DataClassMapRecord(Map<String, DataClass> dataClassMap) {
290+
}
291+
292+
293+
private record DataClassArrayRecord(DataClass[] dataClassArray) {
294+
}
295+
296+
297+
private record IntegerListRecord(List<Integer> integerList) {
252298
}
253299

254300

255-
private record MapDataClass(Map<String, DataClass> dataClassMap) {
301+
private record IntegerMapRecord(Map<String, Integer> integerMap) {
256302
}
257303

258304

259-
private record ArrayDataClass(DataClass[] dataClassArray) {
305+
private record IntegerArrayRecord(Integer[] integerArray) {
260306
}
261307

262308

0 commit comments

Comments
 (0)