Browse Source

Enhance constructor binding for List/Map/Array

Support List/Map/Array of simple values, or values supported
by type conversion.

Closes gh-34305
pull/34387/head
rstoyanchev 12 months ago
parent
commit
462c2bd538
  1. 76
      spring-context/src/main/java/org/springframework/validation/DataBinder.java
  2. 84
      spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java

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

@ -964,7 +964,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -964,7 +964,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
value = createMap(paramPath, paramType, resolvableType, valueResolver);
}
else if (paramType.isArray()) {
value = createArray(paramPath, resolvableType, valueResolver);
value = createArray(paramPath, paramType, resolvableType, valueResolver);
}
}
@ -981,11 +981,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -981,11 +981,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
}
catch (TypeMismatchException ex) {
ex.initPropertyName(paramPath);
args[i] = null;
failedParamNames.add(paramPath);
getBindingResult().recordFieldValue(paramPath, paramType, value);
getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult());
handleTypeMismatchException(ex, paramPath, paramType, value);
}
}
}
@ -1048,9 +1046,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -1048,9 +1046,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
return false;
}
@SuppressWarnings("unchecked")
@Nullable
private <V> List<V> createList(
private List<?> createList(
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
ResolvableType elementType = type.getNested(2);
@ -1058,18 +1055,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -1058,18 +1055,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
if (indexes == null) {
return null;
}
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
List<V> list = (List<V>) CollectionFactory.createCollection(paramType, size);
List<?> list = (List<?>) CollectionFactory.createCollection(paramType, size);
for (int i = 0; i < size; i++) {
list.add(null);
}
for (int index : indexes) {
list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver));
String indexedPath = paramPath + "[" + index + "]";
list.set(index, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
}
return list;
}
@SuppressWarnings("unchecked")
@Nullable
private <V> Map<String, V> createMap(
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
@ -1080,34 +1080,42 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -1080,34 +1080,42 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
if (!name.startsWith(paramPath + "[")) {
continue;
}
int startIdx = paramPath.length() + 1;
int endIdx = name.indexOf(']', startIdx);
String nestedPath = 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) {
map = CollectionFactory.createMap(paramType, 16);
}
if (!map.containsKey(key)) {
map.put(key, (V) createObject(elementType, nestedPath, valueResolver));
}
String indexedPath = name.substring(0, endIdx + 1);
map.put(key, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
}
return map;
}
@SuppressWarnings("unchecked")
@Nullable
private <V> V[] createArray(String paramPath, ResolvableType type, ValueResolver valueResolver) {
private <V> V[] createArray(
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
ResolvableType elementType = type.getNested(2);
SortedSet<Integer> indexes = getIndexes(paramPath, valueResolver);
if (indexes == null) {
return null;
}
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1: 0);
V[] array = (V[]) Array.newInstance(elementType.resolve(), size);
for (int index : indexes) {
array[index] = (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver);
String indexedPath = paramPath + "[" + index + "]";
array[index] = createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver);
}
return array;
}
@ -1118,14 +1126,46 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { @@ -1118,14 +1126,46 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
if (name.startsWith(paramPath + "[")) {
int endIndex = name.indexOf(']', paramPath.length() + 2);
String rawIndex = name.substring(paramPath.length() + 1, endIndex);
int index = Integer.parseInt(rawIndex);
indexes = (indexes != null ? indexes : new TreeSet<>());
indexes.add(index);
if (StringUtils.hasLength(rawIndex)) {
int index = Integer.parseInt(rawIndex);
indexes = (indexes != null ? indexes : new TreeSet<>());
indexes.add(index);
}
}
}
return indexes;
}
@SuppressWarnings("unchecked")
private <V> @Nullable V createIndexedValue(
String paramPath, Class<?> paramType, 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);
}
}
else {
value = createObject(elementType, indexedPath + ".", valueResolver);
}
return (V) value;
}
private void handleTypeMismatchException(
TypeMismatchException ex, String paramPath, Class<?> paramType, @Nullable Object value) {
ex.initPropertyName(paramPath);
getBindingResult().recordFieldValue(paramPath, paramType, value);
getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult());
}
private void validateConstructorArgument(
Class<?> constructorClass, String nestedPath, String name, @Nullable Object value) {

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

@ -103,17 +103,17 @@ class DataBinderConstructTests { @@ -103,17 +103,17 @@ class DataBinderConstructTests {
}
@Test
void listBinding() {
void dataClassWithListBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
"dataClassList[2].param1", "value3", "dataClassList[2].param2", "true"));
DataBinder binder = initDataBinder(ListDataClass.class);
DataBinder binder = initDataBinder(DataClassListRecord.class);
binder.construct(valueResolver);
ListDataClass dataClass = getTarget(binder);
List<DataClass> list = dataClass.dataClassList();
DataClassListRecord target = getTarget(binder);
List<DataClass> list = target.dataClassList();
assertThat(list).hasSize(3);
assertThat(list.get(0).param1()).isEqualTo("value1");
@ -122,17 +122,17 @@ class DataBinderConstructTests { @@ -122,17 +122,17 @@ class DataBinderConstructTests {
}
@Test // gh-34145
void listBindingWithNonconsecutiveIndices() {
void dataClassWithListBindingWithNonconsecutiveIndices() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
"dataClassList[3].param1", "value3", "dataClassList[3].param2", "true"));
DataBinder binder = initDataBinder(ListDataClass.class);
DataBinder binder = initDataBinder(DataClassListRecord.class);
binder.construct(valueResolver);
ListDataClass dataClass = getTarget(binder);
List<DataClass> list = dataClass.dataClassList();
DataClassListRecord target = getTarget(binder);
List<DataClass> list = target.dataClassList();
assertThat(list.get(0).param1()).isEqualTo("value1");
assertThat(list.get(1).param1()).isEqualTo("value2");
@ -140,17 +140,17 @@ class DataBinderConstructTests { @@ -140,17 +140,17 @@ class DataBinderConstructTests {
}
@Test
void mapBinding() {
void dataClassWithMapBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassMap[a].param1", "value1", "dataClassMap[a].param2", "true",
"dataClassMap[b].param1", "value2", "dataClassMap[b].param2", "true",
"dataClassMap['c'].param1", "value3", "dataClassMap['c'].param2", "true"));
DataBinder binder = initDataBinder(MapDataClass.class);
DataBinder binder = initDataBinder(DataClassMapRecord.class);
binder.construct(valueResolver);
MapDataClass dataClass = getTarget(binder);
Map<String, DataClass> map = dataClass.dataClassMap();
DataClassMapRecord target = getTarget(binder);
Map<String, DataClass> map = target.dataClassMap();
assertThat(map).hasSize(3);
assertThat(map.get("a").param1()).isEqualTo("value1");
@ -159,17 +159,17 @@ class DataBinderConstructTests { @@ -159,17 +159,17 @@ class DataBinderConstructTests {
}
@Test
void arrayBinding() {
void dataClassWithArrayBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassArray[0].param1", "value1", "dataClassArray[0].param2", "true",
"dataClassArray[1].param1", "value2", "dataClassArray[1].param2", "true",
"dataClassArray[2].param1", "value3", "dataClassArray[2].param2", "true"));
DataBinder binder = initDataBinder(ArrayDataClass.class);
DataBinder binder = initDataBinder(DataClassArrayRecord.class);
binder.construct(valueResolver);
ArrayDataClass dataClass = getTarget(binder);
DataClass[] array = dataClass.dataClassArray();
DataClassArrayRecord target = getTarget(binder);
DataClass[] array = target.dataClassArray();
assertThat(array).hasSize(3);
assertThat(array[0].param1()).isEqualTo("value1");
@ -177,6 +177,40 @@ class DataBinderConstructTests { @@ -177,6 +177,40 @@ class DataBinderConstructTests {
assertThat(array[2].param1()).isEqualTo("value3");
}
@Test
void simpleListBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerList[0]", "1", "integerList[1]", "2"));
DataBinder binder = initDataBinder(IntegerListRecord.class);
binder.construct(valueResolver);
IntegerListRecord target = getTarget(binder);
assertThat(target.integerList()).containsExactly(1, 2);
}
@Test
void simpleMapBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerMap[a]", "1", "integerMap[b]", "2"));
DataBinder binder = initDataBinder(IntegerMapRecord.class);
binder.construct(valueResolver);
IntegerMapRecord target = getTarget(binder);
assertThat(target.integerMap()).hasSize(2).containsEntry("a", 1).containsEntry("b", 2);
}
@Test
void simpleArrayBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerArray[0]", "1", "integerArray[1]", "2"));
DataBinder binder = initDataBinder(IntegerArrayRecord.class);
binder.construct(valueResolver);
IntegerArrayRecord target = getTarget(binder);
assertThat(target.integerArray()).containsExactly(1, 2);
}
@SuppressWarnings("SameParameterValue")
private static DataBinder initDataBinder(Class<?> targetType) {
DataBinder binder = new DataBinder(null);
@ -248,15 +282,27 @@ class DataBinderConstructTests { @@ -248,15 +282,27 @@ class DataBinderConstructTests {
}
private record ListDataClass(List<DataClass> dataClassList) {
private record DataClassListRecord(List<DataClass> dataClassList) {
}
private record DataClassMapRecord(Map<String, DataClass> dataClassMap) {
}
private record DataClassArrayRecord(DataClass[] dataClassArray) {
}
private record IntegerListRecord(List<Integer> integerList) {
}
private record MapDataClass(Map<String, DataClass> dataClassMap) {
private record IntegerMapRecord(Map<String, Integer> integerMap) {
}
private record ArrayDataClass(DataClass[] dataClassArray) {
private record IntegerArrayRecord(Integer[] integerArray) {
}

Loading…
Cancel
Save