Skip to content

Commit b0dedbe

Browse files
committed
Polish "Recursively instantiate arguments"
Closes gh-147
1 parent 77f5875 commit b0dedbe

File tree

3 files changed

+47
-21
lines changed

3 files changed

+47
-21
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,8 @@ public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment
8080
if (CollectionFactory.isApproximableCollectionType(rawValue.getClass())) {
8181
Assert.isAssignable(Collection.class, parameterType.getType(),
8282
"Argument '" + name + "' is a Collection while the @Argument method parameter is " + parameterType.getType());
83-
Collection<Object> rawCollection = (Collection<Object>) rawValue;
84-
Collection<Object> values = CollectionFactory.createApproximateCollection(rawValue, rawCollection.size());
8583
Class<?> elementType = parameterType.getElementTypeDescriptor().getType();
86-
rawCollection.forEach(item -> values.add(convert(item, elementType)));
87-
return values;
84+
return this.instantiator.instantiateCollection(elementType, (Collection<Object>) rawValue);
8885
}
8986

9087
MethodParameter nestedParameter = parameter.nestedIfOptional();
@@ -103,7 +100,7 @@ private Object returnValue(Object value, Class<?> parameterType) {
103100
private Object convert(Object rawValue, Class<?> targetType) {
104101
Object target;
105102
if (rawValue instanceof Map) {
106-
target = this.instantiator.instantiate(targetType, (Map<String, Object>) rawValue);
103+
target = this.instantiator.instantiate((Map<String, Object>) rawValue, targetType);
107104
}
108105
else if (targetType.isAssignableFrom(rawValue.getClass())) {
109106
return returnValue(rawValue, targetType);

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentInstantiator.java

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626

2727
import org.springframework.beans.BeanUtils;
2828
import org.springframework.beans.MutablePropertyValues;
29-
import org.springframework.beans.PropertyValues;
3029
import org.springframework.core.CollectionFactory;
3130
import org.springframework.core.MethodParameter;
3231
import org.springframework.core.convert.TypeDescriptor;
32+
import org.springframework.util.Assert;
3333
import org.springframework.validation.DataBinder;
3434

3535
/**
@@ -40,20 +40,22 @@
4040
*/
4141
class GraphQlArgumentInstantiator {
4242

43+
private final DataBinder converter = new DataBinder(null);
44+
4345
/**
4446
* Instantiate the given target type and bind data from
4547
* {@link graphql.schema.DataFetchingEnvironment} arguments.
4648
* <p>This is considering the default constructor or a primary constructor
4749
* if available.
4850
*
49-
* @param targetType the type of the argument to instantiate
5051
* @param arguments the data fetching environment arguments
52+
* @param targetType the type of the argument to instantiate
5153
* @param <T> the type of the input argument
5254
* @return the instantiated and populated input argument.
5355
* @throws IllegalStateException if there is no suitable constructor.
5456
*/
5557
@SuppressWarnings("unchecked")
56-
public <T> T instantiate(Class<T> targetType, Map<String, Object> arguments) {
58+
public <T> T instantiate(Map<String, Object> arguments, Class<T> targetType) {
5759
Object target;
5860
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetType);
5961

@@ -65,7 +67,6 @@ public <T> T instantiate(Class<T> targetType, Map<String, Object> arguments) {
6567
}
6668
else {
6769
// Data class constructor
68-
DataBinder binder = new DataBinder(null);
6970
String[] paramNames = BeanUtils.getParameterNames(ctor);
7071
Class<?>[] paramTypes = ctor.getParameterTypes();
7172
Object[] args = new Object[paramTypes.length];
@@ -77,23 +78,51 @@ public <T> T instantiate(Class<T> targetType, Map<String, Object> arguments) {
7778
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
7879
}
7980
else if (value != null && CollectionFactory.isApproximableCollectionType(value.getClass())) {
80-
Collection<Object> rawCollection = (Collection<Object>) value;
81-
Collection<Object> values = CollectionFactory.createApproximateCollection(value, rawCollection.size());
82-
8381
TypeDescriptor typeDescriptor = new TypeDescriptor(methodParam);
8482
Class<?> elementType = typeDescriptor.getElementTypeDescriptor().getType();
85-
rawCollection.forEach(item -> values.add(this.instantiate(elementType, (Map<String, Object>)item)));
86-
args[i] = values;
83+
args[i] = instantiateCollection(elementType, (Collection<Object>) value);
8784
}
8885
else {
89-
args[i] = binder.convertIfNecessary(value, paramTypes[i], methodParam);
86+
args[i] = this.converter.convertIfNecessary(value, paramTypes[i], methodParam);
9087
}
9188
}
9289
target = BeanUtils.instantiateClass(ctor, args);
9390
}
9491
return (T) target;
9592
}
9693

94+
/**
95+
* Instantiate a collection of {@code elementType} using the given {@code values}.
96+
* <p>This will instantiate a new Collection of the closest type possible
97+
* from the one provided as an argument.
98+
*
99+
* @param elementType the type of elements in the given Collection
100+
* @param values the collection of values to bind and instantiate
101+
* @param <T> the type of Collection elements
102+
* @return the instantiated and populated Collection.
103+
* @throws IllegalStateException if there is no suitable constructor.
104+
*/
105+
@SuppressWarnings("unchecked")
106+
public <T> Collection<T> instantiateCollection(Class<T> elementType, Collection<Object> values) {
107+
Assert.state(CollectionFactory.isApproximableCollectionType(values.getClass()),
108+
() -> "Cannot instantiate Collection for type " + values.getClass());
109+
Collection<T> instances = CollectionFactory.createApproximateCollection(values, values.size());
110+
values.forEach(item -> {
111+
T value;
112+
if (elementType.isAssignableFrom(item.getClass())) {
113+
value = (T) item;
114+
}
115+
else if (item instanceof Map) {
116+
value = this.instantiate((Map<String, Object>)item, elementType);
117+
}
118+
else {
119+
value = this.converter.convertIfNecessary(item, elementType);
120+
}
121+
instances.add(value);
122+
});
123+
return instances;
124+
}
125+
97126
/**
98127
* Perform a Depth First Search in the given JSON map to collect attribute values
99128
* as {@link MutablePropertyValues} using the full property path as key.

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentInstantiatorTests.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class GraphQlArgumentInstantiatorTests {
4646
void shouldInstantiateDefaultConstructor() throws Exception {
4747
String payload = "{\"simpleBean\": { \"name\": \"test\"} }";
4848
DataFetchingEnvironment environment = initEnvironment(payload);
49-
SimpleBean result = instantiator.instantiate(SimpleBean.class, environment.getArgument("simpleBean"));
49+
SimpleBean result = instantiator.instantiate(environment.getArgument("simpleBean"), SimpleBean.class);
5050

5151
assertThat(result).isNotNull().isInstanceOf(SimpleBean.class);
5252
assertThat(result).hasFieldOrPropertyWithValue("name", "test");
@@ -56,7 +56,7 @@ void shouldInstantiateDefaultConstructor() throws Exception {
5656
void shouldInstantiatePrimaryConstructor() throws Exception {
5757
String payload = "{\"constructorBean\": { \"name\": \"test\"} }";
5858
DataFetchingEnvironment environment = initEnvironment(payload);
59-
ContructorBean result = instantiator.instantiate(ContructorBean.class, environment.getArgument("constructorBean"));
59+
ContructorBean result = instantiator.instantiate(environment.getArgument("constructorBean"), ContructorBean.class);
6060

6161
assertThat(result).isNotNull().isInstanceOf(ContructorBean.class);
6262
assertThat(result).hasFieldOrPropertyWithValue("name", "test");
@@ -66,15 +66,15 @@ void shouldInstantiatePrimaryConstructor() throws Exception {
6666
void shouldFailIfNoPrimaryConstructor() throws Exception {
6767
String payload = "{\"noPrimary\": { \"name\": \"test\"} }";
6868
DataFetchingEnvironment environment = initEnvironment(payload);
69-
assertThatThrownBy(() -> instantiator.instantiate(NoPrimaryConstructor.class, environment.getArgument("noPrimary")))
69+
assertThatThrownBy(() -> instantiator.instantiate(environment.getArgument("noPrimary"), NoPrimaryConstructor.class))
7070
.isInstanceOf(IllegalStateException.class).hasMessageContaining("No primary or single public constructor found");
7171
}
7272

7373
@Test
7474
void shouldInstantiateNestedBean() throws Exception {
7575
String payload = "{\"book\": { \"name\": \"test name\", \"author\": { \"firstName\": \"Jane\", \"lastName\": \"Spring\"} } }";
7676
DataFetchingEnvironment environment = initEnvironment(payload);
77-
Book result = instantiator.instantiate(Book.class, environment.getArgument("book"));
77+
Book result = instantiator.instantiate(environment.getArgument("book"), Book.class);
7878

7979
assertThat(result).isNotNull().isInstanceOf(Book.class);
8080
assertThat(result).hasFieldOrPropertyWithValue("name", "test name");
@@ -87,7 +87,7 @@ void shouldInstantiateNestedBean() throws Exception {
8787
void shouldInstantiateNestedBeanLists() throws Exception {
8888
String payload = "{\"nestedList\": { \"items\": [ {\"name\": \"first\"}, {\"name\": \"second\"}] } }";
8989
DataFetchingEnvironment environment = initEnvironment(payload);
90-
NestedList result = instantiator.instantiate(NestedList.class, environment.getArgument("nestedList"));
90+
NestedList result = instantiator.instantiate(environment.getArgument("nestedList"), NestedList.class);
9191

9292
assertThat(result).isNotNull().isInstanceOf(NestedList.class);
9393
assertThat(result.getItems()).hasSize(2).extracting("name").containsExactly("first", "second");
@@ -97,7 +97,7 @@ void shouldInstantiateNestedBeanLists() throws Exception {
9797
void shouldInstantiatePrimaryConstructorNestedBeanLists() throws Exception {
9898
String payload = "{\"nestedList\": { \"items\": [ {\"name\": \"first\"}, {\"name\": \"second\"}] } }";
9999
DataFetchingEnvironment environment = initEnvironment(payload);
100-
PrimaryConstructorNestedList result = instantiator.instantiate(PrimaryConstructorNestedList.class, environment.getArgument("nestedList"));
100+
PrimaryConstructorNestedList result = instantiator.instantiate(environment.getArgument("nestedList"), PrimaryConstructorNestedList.class);
101101

102102
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorNestedList.class);
103103
assertThat(result.getItems()).hasSize(2).extracting("name").containsExactly("first", "second");

0 commit comments

Comments
 (0)