Skip to content

Commit e48cbc5

Browse files
committedJun 24, 2024
Support list/map/array constructor data binding
See gh-32426
1 parent f9af5d4 commit e48cbc5

File tree

2 files changed

+165
-11
lines changed

2 files changed

+165
-11
lines changed
 

Diff for: ‎spring-context/src/main/java/org/springframework/validation/DataBinder.java

+96-6
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,20 @@
1818

1919
import java.beans.PropertyEditor;
2020
import java.lang.annotation.Annotation;
21+
import java.lang.reflect.Array;
2122
import java.lang.reflect.Constructor;
2223
import java.lang.reflect.Field;
2324
import java.util.ArrayList;
2425
import java.util.Arrays;
25-
import java.util.Collection;
2626
import java.util.Collections;
2727
import java.util.HashMap;
2828
import java.util.HashSet;
2929
import java.util.List;
3030
import java.util.Map;
3131
import java.util.Optional;
3232
import java.util.Set;
33+
import java.util.SortedSet;
34+
import java.util.TreeSet;
3335
import java.util.function.Predicate;
3436

3537
import org.apache.commons.logging.Log;
@@ -49,6 +51,7 @@
4951
import org.springframework.beans.SimpleTypeConverter;
5052
import org.springframework.beans.TypeConverter;
5153
import org.springframework.beans.TypeMismatchException;
54+
import org.springframework.core.CollectionFactory;
5255
import org.springframework.core.KotlinDetector;
5356
import org.springframework.core.MethodParameter;
5457
import org.springframework.core.ResolvableType;
@@ -948,11 +951,24 @@ private Object createObject(ResolvableType objectType, String nestedPath, ValueR
948951

949952
String paramPath = nestedPath + lookupName;
950953
Class<?> paramType = paramTypes[i];
954+
ResolvableType resolvableType = ResolvableType.forMethodParameter(param);
955+
951956
Object value = valueResolver.resolveValue(paramPath, paramType);
952957

958+
if (value == null) {
959+
if (List.class.isAssignableFrom(paramType)) {
960+
value = createList(paramPath, paramType, resolvableType, valueResolver);
961+
}
962+
else if (Map.class.isAssignableFrom(paramType)) {
963+
value = createMap(paramPath, paramType, resolvableType, valueResolver);
964+
}
965+
else if (paramType.isArray()) {
966+
value = createArray(paramPath, resolvableType, valueResolver);
967+
}
968+
}
969+
953970
if (value == null && shouldConstructArgument(param) && hasValuesFor(paramPath, valueResolver)) {
954-
ResolvableType type = ResolvableType.forMethodParameter(param);
955-
args[i] = createObject(type, paramPath + ".", valueResolver);
971+
args[i] = createObject(resolvableType, paramPath + ".", valueResolver);
956972
}
957973
else {
958974
try {
@@ -1019,9 +1035,7 @@ private Object createObject(ResolvableType objectType, String nestedPath, ValueR
10191035
*/
10201036
protected boolean shouldConstructArgument(MethodParameter param) {
10211037
Class<?> type = param.nestedIfOptional().getNestedParameterType();
1022-
return !(BeanUtils.isSimpleValueType(type) ||
1023-
Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) || type.isArray() ||
1024-
type.getPackageName().startsWith("java."));
1038+
return !BeanUtils.isSimpleValueType(type) && !type.getPackageName().startsWith("java.");
10251039
}
10261040

10271041
private boolean hasValuesFor(String paramPath, ValueResolver resolver) {
@@ -1033,6 +1047,82 @@ private boolean hasValuesFor(String paramPath, ValueResolver resolver) {
10331047
return false;
10341048
}
10351049

1050+
@SuppressWarnings("unchecked")
1051+
@Nullable
1052+
private <V> List<V> createList(
1053+
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
1054+
1055+
ResolvableType elementType = type.getNested(2);
1056+
SortedSet<Integer> indexes = getIndexes(paramPath, valueResolver);
1057+
if (indexes == null) {
1058+
return null;
1059+
}
1060+
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
1061+
List<V> list = (List<V>) CollectionFactory.createCollection(paramType, size);
1062+
indexes.forEach(i -> list.add(null));
1063+
for (int index : indexes) {
1064+
list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver));
1065+
}
1066+
return list;
1067+
}
1068+
1069+
@SuppressWarnings("unchecked")
1070+
@Nullable
1071+
private <V> Map<String, V> createMap(
1072+
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
1073+
1074+
ResolvableType elementType = type.getNested(2);
1075+
Map<String, V> map = null;
1076+
for (String name : valueResolver.getNames()) {
1077+
if (!name.startsWith(paramPath + "[")) {
1078+
continue;
1079+
}
1080+
int startIdx = paramPath.length() + 1;
1081+
int endIdx = name.indexOf(']', startIdx);
1082+
String nestedPath = name.substring(0, endIdx + 2);
1083+
boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\'');
1084+
String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx));
1085+
if (map == null) {
1086+
map = CollectionFactory.createMap(paramType, 16);
1087+
}
1088+
if (!map.containsKey(key)) {
1089+
map.put(key, (V) createObject(elementType, nestedPath, valueResolver));
1090+
}
1091+
}
1092+
return map;
1093+
}
1094+
1095+
@SuppressWarnings("unchecked")
1096+
@Nullable
1097+
private <V> V[] createArray(String paramPath, ResolvableType type, ValueResolver valueResolver) {
1098+
ResolvableType elementType = type.getNested(2);
1099+
SortedSet<Integer> indexes = getIndexes(paramPath, valueResolver);
1100+
if (indexes == null) {
1101+
return null;
1102+
}
1103+
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1: 0);
1104+
V[] array = (V[]) Array.newInstance(elementType.resolve(), size);
1105+
for (int index : indexes) {
1106+
array[index] = (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver);
1107+
}
1108+
return array;
1109+
}
1110+
1111+
@Nullable
1112+
private static SortedSet<Integer> getIndexes(String paramPath, ValueResolver valueResolver) {
1113+
SortedSet<Integer> indexes = null;
1114+
for (String name : valueResolver.getNames()) {
1115+
if (name.startsWith(paramPath + "[")) {
1116+
int endIndex = name.indexOf(']', paramPath.length() + 2);
1117+
String rawIndex = name.substring(paramPath.length() + 1, endIndex);
1118+
int index = Integer.parseInt(rawIndex);
1119+
indexes = (indexes != null ? indexes : new TreeSet<>());
1120+
indexes.add(index);
1121+
}
1122+
}
1123+
return indexes;
1124+
}
1125+
10361126
private void validateConstructorArgument(
10371127
Class<?> constructorClass, String nestedPath, String name, @Nullable Object value) {
10381128

Diff for: ‎spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java

+69-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.validation;
1818

1919
import java.beans.ConstructorProperties;
20+
import java.util.List;
2021
import java.util.Map;
2122
import java.util.Optional;
2223
import java.util.Set;
@@ -101,6 +102,63 @@ void dataClassBindingWithConversionError() {
101102
assertThat(bindingResult.getFieldValue("param3")).isNull();
102103
}
103104

105+
@Test
106+
void listBinding() {
107+
MapValueResolver valueResolver = new MapValueResolver(Map.of(
108+
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
109+
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
110+
"dataClassList[2].param1", "value3", "dataClassList[2].param2", "true"));
111+
112+
DataBinder binder = initDataBinder(ListDataClass.class);
113+
binder.construct(valueResolver);
114+
115+
ListDataClass dataClass = getTarget(binder);
116+
List<DataClass> list = dataClass.dataClassList();
117+
118+
assertThat(list).hasSize(3);
119+
assertThat(list.get(0).param1()).isEqualTo("value1");
120+
assertThat(list.get(1).param1()).isEqualTo("value2");
121+
assertThat(list.get(2).param1()).isEqualTo("value3");
122+
}
123+
124+
@Test
125+
void mapBinding() {
126+
MapValueResolver valueResolver = new MapValueResolver(Map.of(
127+
"dataClassMap[a].param1", "value1", "dataClassMap[a].param2", "true",
128+
"dataClassMap[b].param1", "value2", "dataClassMap[b].param2", "true",
129+
"dataClassMap['c'].param1", "value3", "dataClassMap['c'].param2", "true"));
130+
131+
DataBinder binder = initDataBinder(MapDataClass.class);
132+
binder.construct(valueResolver);
133+
134+
MapDataClass dataClass = getTarget(binder);
135+
Map<String, DataClass> map = dataClass.dataClassMap();
136+
137+
assertThat(map).hasSize(3);
138+
assertThat(map.get("a").param1()).isEqualTo("value1");
139+
assertThat(map.get("b").param1()).isEqualTo("value2");
140+
assertThat(map.get("c").param1()).isEqualTo("value3");
141+
}
142+
143+
@Test
144+
void arrayBinding() {
145+
MapValueResolver valueResolver = new MapValueResolver(Map.of(
146+
"dataClassArray[0].param1", "value1", "dataClassArray[0].param2", "true",
147+
"dataClassArray[1].param1", "value2", "dataClassArray[1].param2", "true",
148+
"dataClassArray[2].param1", "value3", "dataClassArray[2].param2", "true"));
149+
150+
DataBinder binder = initDataBinder(ArrayDataClass.class);
151+
binder.construct(valueResolver);
152+
153+
ArrayDataClass dataClass = getTarget(binder);
154+
DataClass[] array = dataClass.dataClassArray();
155+
156+
assertThat(array).hasSize(3);
157+
assertThat(array[0].param1()).isEqualTo("value1");
158+
assertThat(array[1].param1()).isEqualTo("value2");
159+
assertThat(array[2].param1()).isEqualTo("value3");
160+
}
161+
104162
@SuppressWarnings("SameParameterValue")
105163
private static DataBinder initDataBinder(Class<?> targetType) {
106164
DataBinder binder = new DataBinder(null);
@@ -172,13 +230,19 @@ public DataClass nestedParam2() {
172230
}
173231

174232

175-
private static class MapValueResolver implements DataBinder.ValueResolver {
233+
private record ListDataClass(List<DataClass> dataClassList) {
234+
}
235+
236+
237+
private record MapDataClass(Map<String, DataClass> dataClassMap) {
238+
}
176239

177-
private final Map<String, Object> map;
178240

179-
private MapValueResolver(Map<String, Object> map) {
180-
this.map = map;
181-
}
241+
private record ArrayDataClass(DataClass[] dataClassArray) {
242+
}
243+
244+
245+
private record MapValueResolver(Map<String, Object> map) implements DataBinder.ValueResolver {
182246

183247
@Override
184248
public Object resolveValue(String name, Class<?> type) {

0 commit comments

Comments
 (0)
Please sign in to comment.