Recursively instantiate input arguments

Prior to this commit, the `GraphQlArgumentInstantiator` would not
instantiate and bind properly types with a list attribute that has
elements which need to be instantiated with a primary constructor.

This commit ensures that every list that is encountered is
recursively passed to the instantiator, instead of trying to
convert the list using the databinder.

This also fixes gh-145, by directly accessing the arguments from
the arguments map, instead of the mapped propertyvalues used for
the DataBinder.

See gh-147
This commit is contained in:
Koen Punt
2021-09-21 11:08:22 +02:00
committed by Brian Clozel
parent 552c4f72d0
commit 77f5875eac
2 changed files with 38 additions and 3 deletions

View File

@@ -17,6 +17,7 @@
package org.springframework.graphql.data.method.annotation.support;
import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -25,7 +26,10 @@ import java.util.Stack;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.core.CollectionFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.validation.DataBinder;
/**
@@ -52,9 +56,9 @@ class GraphQlArgumentInstantiator {
public <T> T instantiate(Class<T> targetType, Map<String, Object> arguments) {
Object target;
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetType);
MutablePropertyValues propertyValues = extractPropertyValues(arguments);
if (ctor.getParameterCount() == 0) {
MutablePropertyValues propertyValues = extractPropertyValues(arguments);
target = BeanUtils.instantiateClass(ctor);
DataBinder dataBinder = new DataBinder(target);
dataBinder.bind(propertyValues);
@@ -67,12 +71,20 @@ class GraphQlArgumentInstantiator {
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
Object value = propertyValues.get(paramName);
value = (value instanceof List ? ((List<?>) value).toArray() : value);
Object value = arguments.get(paramName);
MethodParameter methodParam = new MethodParameter(ctor, i);
if (value == null && methodParam.isOptional()) {
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
}
else if (value != null && CollectionFactory.isApproximableCollectionType(value.getClass())) {
Collection<Object> rawCollection = (Collection<Object>) value;
Collection<Object> values = CollectionFactory.createApproximateCollection(value, rawCollection.size());
TypeDescriptor typeDescriptor = new TypeDescriptor(methodParam);
Class<?> elementType = typeDescriptor.getElementTypeDescriptor().getType();
rawCollection.forEach(item -> values.add(this.instantiate(elementType, (Map<String, Object>)item)));
args[i] = values;
}
else {
args[i] = binder.convertIfNecessary(value, paramTypes[i], methodParam);
}

View File

@@ -93,6 +93,16 @@ class GraphQlArgumentInstantiatorTests {
assertThat(result.getItems()).hasSize(2).extracting("name").containsExactly("first", "second");
}
@Test
void shouldInstantiatePrimaryConstructorNestedBeanLists() throws Exception {
String payload = "{\"nestedList\": { \"items\": [ {\"name\": \"first\"}, {\"name\": \"second\"}] } }";
DataFetchingEnvironment environment = initEnvironment(payload);
PrimaryConstructorNestedList result = instantiator.instantiate(PrimaryConstructorNestedList.class, environment.getArgument("nestedList"));
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorNestedList.class);
assertThat(result.getItems()).hasSize(2).extracting("name").containsExactly("first", "second");
}
private DataFetchingEnvironment initEnvironment(String jsonPayload) throws JsonProcessingException {
Map<String, Object> arguments = this.mapper.readValue(jsonPayload, new TypeReference<Map<String, Object>>() {
});
@@ -147,6 +157,19 @@ class GraphQlArgumentInstantiatorTests {
}
}
static class PrimaryConstructorNestedList {
final List<Item> items;
public PrimaryConstructorNestedList(List<Item> items) {
this.items = items;
}
public List<Item> getItems() {
return items;
}
}
static class Item {
String name;