Skip to content

Commit 5919178

Browse files
committed
Refactoring in GraphQL argument initialization
Consolidate GraphQL argument initialization by pushing logic from ArgumentMethodArgumentResolver down into GraphQlArgumentInitializer, which now takes the DataFetchingEnvironment, an optional argument name, and a target type, and does the rest of the work.
1 parent b22cef0 commit 5919178

File tree

6 files changed

+152
-125
lines changed

6 files changed

+152
-125
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentInitializer.java

Lines changed: 105 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
import java.util.Optional;
2525
import java.util.Stack;
2626

27+
import graphql.schema.DataFetchingEnvironment;
28+
2729
import org.springframework.beans.BeanUtils;
2830
import org.springframework.beans.MutablePropertyValues;
2931
import org.springframework.beans.SimpleTypeConverter;
30-
import org.springframework.beans.TypeConverter;
3132
import org.springframework.core.CollectionFactory;
3233
import org.springframework.core.MethodParameter;
34+
import org.springframework.core.ResolvableType;
3335
import org.springframework.core.convert.ConversionService;
34-
import org.springframework.core.convert.TypeDescriptor;
3536
import org.springframework.lang.Nullable;
3637
import org.springframework.util.Assert;
3738
import org.springframework.validation.DataBinder;
@@ -56,27 +57,108 @@ public GraphQlArgumentInitializer(@Nullable ConversionService conversionService)
5657

5758

5859
/**
59-
* Return the underlying {@link DataBinder}.
60+
* Initialize an Object of the given {@code targetType}, either from a named
61+
* {@link DataFetchingEnvironment#getArgument(String) argument value}, or from all
62+
* {@link DataFetchingEnvironment#getArguments() values} as the source.
63+
* @param environment the environment with the argument values
64+
* @param name optionally, the name of an argument to initialize from,
65+
* or if {@code null}, the full map of arguments is used.
66+
* @param targetType the type of Object to initialize
67+
* @return the initialized Object, or {@code null}
6068
*/
61-
public TypeConverter getTypeConverter() {
62-
return this.typeConverter;
69+
@Nullable
70+
@SuppressWarnings("unchecked")
71+
public Object initializeArgument(
72+
DataFetchingEnvironment environment, @Nullable String name, ResolvableType targetType) {
73+
74+
Object sourceValue = (name != null ? environment.getArgument(name) : environment.getArguments());
75+
76+
if (sourceValue == null) {
77+
return wrapAsOptionalIfNecessary(null, targetType);
78+
}
79+
80+
Class<?> targetClass = targetType.resolve();
81+
Assert.notNull(targetClass, "Could not determine target type from " + targetType);
82+
83+
// From Collection
84+
85+
if (CollectionFactory.isApproximableCollectionType(sourceValue.getClass())) {
86+
Assert.isAssignable(Collection.class, targetClass,
87+
"Argument '" + name + "' is a Collection while method parameter is " + targetClass.getName());
88+
Class<?> elementType = targetType.asCollection().getGeneric(0).resolve();
89+
Assert.notNull(elementType, "Could not determine element type for " + targetType);
90+
return initializeFromCollection((Collection<Object>) sourceValue, elementType);
91+
}
92+
93+
if (targetClass == Optional.class) {
94+
targetClass = targetType.getNested(2).resolve();
95+
Assert.notNull(targetClass, "Could not determine Optional<T> type from " + targetType);
96+
}
97+
98+
// From Map
99+
100+
if (sourceValue instanceof Map) {
101+
Object target = initializeFromMap((Map<String, Object>) sourceValue, targetClass);
102+
return wrapAsOptionalIfNecessary(target, targetType);
103+
}
104+
105+
// From Scalar
106+
107+
if (targetClass.isInstance(sourceValue)) {
108+
return wrapAsOptionalIfNecessary(sourceValue, targetType);
109+
}
110+
111+
Object target = this.typeConverter.convertIfNecessary(sourceValue, targetClass);
112+
if (target == null) {
113+
throw new IllegalStateException("Cannot convert argument value " +
114+
"type [" + sourceValue.getClass().getName() + "] to method parameter " +
115+
"type [" + targetClass.getName() + "].");
116+
}
117+
118+
return wrapAsOptionalIfNecessary(target, targetType);
63119
}
64120

121+
@Nullable
122+
private Object wrapAsOptionalIfNecessary(@Nullable Object value, ResolvableType type) {
123+
return (type.resolve(Object.class).equals(Optional.class) ? Optional.ofNullable(value) : value);
124+
}
125+
126+
/**
127+
* Instantiate a collection of {@code elementType} using the given {@code values}.
128+
* <p>This will instantiate a new Collection of the closest type possible
129+
* from the one provided as an argument.
130+
*
131+
* @param <T> the type of Collection elements
132+
* @param values the collection of values to bind and instantiate
133+
* @param elementClass the type of elements in the given Collection
134+
* @return the instantiated and populated Collection.
135+
* @throws IllegalStateException if there is no suitable constructor.
136+
*/
137+
@SuppressWarnings("unchecked")
138+
private <T> Collection<T> initializeFromCollection(Collection<Object> values, Class<T> elementClass) {
139+
Collection<T> collection = CollectionFactory.createApproximateCollection(values, values.size());
140+
for (Object item : values) {
141+
if (elementClass.isAssignableFrom(item.getClass())) {
142+
collection.add((T) item);
143+
}
144+
else if (item instanceof Map) {
145+
collection.add((T) this.initializeFromMap((Map<String, Object>) item, elementClass));
146+
}
147+
else {
148+
collection.add(this.typeConverter.convertIfNecessary(item, elementClass));
149+
}
150+
}
151+
return collection;
152+
}
65153

66154
/**
67155
* Instantiate an Object of the given target type and bind
68156
* {@link graphql.schema.DataFetchingEnvironment} argument values to it.
69-
* This considers using the default constructor or a primary constructor,
70-
* if available.
71-
*
72-
* @param arguments the data fetching environment arguments
73-
* @param targetType the type of the argument to instantiate
74-
* @param <T> the type of the input argument
75-
* @return the instantiated and populated input argument.
157+
* This considers the default constructor or a primary constructor, if available.
76158
* @throws IllegalStateException if there is no suitable constructor.
77159
*/
78160
@SuppressWarnings("unchecked")
79-
public <T> T initializeFromMap(Map<String, Object> arguments, Class<T> targetType) {
161+
private Object initializeFromMap(Map<String, Object> arguments, Class<?> targetType) {
80162
Object target;
81163
Constructor<?> ctor = BeanUtils.getResolvableConstructor(targetType);
82164

@@ -85,7 +167,7 @@ public <T> T initializeFromMap(Map<String, Object> arguments, Class<T> targetTyp
85167
target = BeanUtils.instantiateClass(ctor);
86168
DataBinder dataBinder = new DataBinder(target);
87169
dataBinder.bind(propertyValues);
88-
return (T) target;
170+
return target;
89171
}
90172

91173
// Data class constructor
@@ -96,56 +178,25 @@ public <T> T initializeFromMap(Map<String, Object> arguments, Class<T> targetTyp
96178
for (int i = 0; i < paramNames.length; i++) {
97179
String paramName = paramNames[i];
98180
Object value = arguments.get(paramName);
99-
MethodParameter methodParam = new MethodParameter(ctor, i);
100-
if (value == null && methodParam.isOptional()) {
101-
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
181+
MethodParameter methodParameter = new MethodParameter(ctor, i);
182+
if (value == null && methodParameter.isOptional()) {
183+
args[i] = (methodParameter.getParameterType() == Optional.class ? Optional.empty() : null);
102184
}
103185
else if (value != null && CollectionFactory.isApproximableCollectionType(value.getClass())) {
104-
TypeDescriptor typeDescriptor = new TypeDescriptor(methodParam);
105-
Class<?> elementType = typeDescriptor.getElementTypeDescriptor().getType();
186+
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
187+
Class<?> elementType = resolvableType.asCollection().getGeneric(0).resolve();
188+
Assert.notNull(elementType, "Cannot determine element type for " + resolvableType);
106189
args[i] = initializeFromCollection((Collection<Object>) value, elementType);
107190
}
108191
else if (value instanceof Map) {
109-
args[i] = this.initializeFromMap((Map<String, Object>) value, methodParam.getParameterType());
192+
args[i] = this.initializeFromMap((Map<String, Object>) value, methodParameter.getParameterType());
110193
}
111194
else {
112-
args[i] = this.typeConverter.convertIfNecessary(value, paramTypes[i], methodParam);
195+
args[i] = this.typeConverter.convertIfNecessary(value, paramTypes[i], methodParameter);
113196
}
114197
}
115198

116-
return (T) BeanUtils.instantiateClass(ctor, args);
117-
}
118-
119-
/**
120-
* Instantiate a collection of {@code elementType} using the given {@code values}.
121-
* <p>This will instantiate a new Collection of the closest type possible
122-
* from the one provided as an argument.
123-
*
124-
* @param <T> the type of Collection elements
125-
* @param values the collection of values to bind and instantiate
126-
* @param elementType the type of elements in the given Collection
127-
* @return the instantiated and populated Collection.
128-
* @throws IllegalStateException if there is no suitable constructor.
129-
*/
130-
@SuppressWarnings("unchecked")
131-
public <T> Collection<T> initializeFromCollection(Collection<Object> values, Class<T> elementType) {
132-
Assert.state(CollectionFactory.isApproximableCollectionType(values.getClass()),
133-
() -> "Cannot instantiate Collection for type " + values.getClass());
134-
Collection<T> instances = CollectionFactory.createApproximateCollection(values, values.size());
135-
values.forEach(item -> {
136-
T value;
137-
if (elementType.isAssignableFrom(item.getClass())) {
138-
value = (T) item;
139-
}
140-
else if (item instanceof Map) {
141-
value = this.initializeFromMap((Map<String, Object>)item, elementType);
142-
}
143-
else {
144-
value = this.typeConverter.convertIfNecessary(item, elementType);
145-
}
146-
instances.add(value);
147-
});
148-
return instances;
199+
return BeanUtils.instantiateClass(ctor, args);
149200
}
150201

151202
/**

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.core.MethodParameter;
4747
import org.springframework.core.annotation.AnnotatedElementUtils;
4848
import org.springframework.core.convert.ConversionService;
49+
import org.springframework.graphql.data.GraphQlArgumentInitializer;
4950
import org.springframework.graphql.data.method.HandlerMethod;
5051
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
5152
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
@@ -128,7 +129,8 @@ public void afterPropertiesSet() {
128129
this.argumentResolvers.addResolver(new ProjectedPayloadMethodArgumentResolver());
129130
}
130131
this.argumentResolvers.addResolver(new ArgumentMapMethodArgumentResolver());
131-
this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(this.conversionService));
132+
GraphQlArgumentInitializer initializer = new GraphQlArgumentInitializer(this.conversionService);
133+
this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(initializer));
132134
this.argumentResolvers.addResolver(new ContextValueMethodArgumentResolver());
133135

134136
// Type based

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

Lines changed: 6 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,13 @@
1515
*/
1616
package org.springframework.graphql.data.method.annotation.support;
1717

18-
import java.util.Collection;
19-
import java.util.Map;
20-
import java.util.Optional;
21-
2218
import graphql.schema.DataFetchingEnvironment;
2319

24-
import org.springframework.core.CollectionFactory;
2520
import org.springframework.core.MethodParameter;
26-
import org.springframework.core.convert.ConversionService;
27-
import org.springframework.core.convert.TypeDescriptor;
21+
import org.springframework.core.ResolvableType;
2822
import org.springframework.graphql.data.GraphQlArgumentInitializer;
2923
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
3024
import org.springframework.graphql.data.method.annotation.Argument;
31-
import org.springframework.lang.Nullable;
3225
import org.springframework.util.Assert;
3326
import org.springframework.util.StringUtils;
3427

@@ -46,8 +39,9 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
4639
private final GraphQlArgumentInitializer argumentInitializer;
4740

4841

49-
public ArgumentMethodArgumentResolver(@Nullable ConversionService conversionService) {
50-
this.argumentInitializer = new GraphQlArgumentInitializer(conversionService);
42+
public ArgumentMethodArgumentResolver(GraphQlArgumentInitializer initializer) {
43+
Assert.notNull(initializer, "GraphQlArgumentInitializer is required");
44+
this.argumentInitializer = initializer;
5145
}
5246

5347

@@ -57,47 +51,10 @@ public boolean supportsParameter(MethodParameter parameter) {
5751
}
5852

5953
@Override
60-
@SuppressWarnings("unchecked")
6154
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
6255
String name = getArgumentName(parameter);
63-
Object rawValue = environment.getArgument(name);
64-
TypeDescriptor typeDescriptor = new TypeDescriptor(parameter);
65-
66-
if (rawValue == null) {
67-
return wrapAsOptionalIfNecessary(null, typeDescriptor.getType());
68-
}
69-
70-
// From Collection
71-
72-
if (CollectionFactory.isApproximableCollectionType(rawValue.getClass())) {
73-
Assert.isAssignable(Collection.class, typeDescriptor.getType(),
74-
"Argument '" + name + "' is a Collection " +
75-
"while the @Argument method parameter is " + typeDescriptor.getType());
76-
Class<?> elementType = typeDescriptor.getElementTypeDescriptor().getType();
77-
return this.argumentInitializer.initializeFromCollection((Collection<Object>) rawValue, elementType);
78-
}
79-
80-
Class<?> targetType = parameter.nestedIfOptional().getNestedParameterType();
81-
Object target;
82-
83-
// From Map
84-
85-
if (rawValue instanceof Map) {
86-
target = this.argumentInitializer.initializeFromMap((Map<String, Object>) rawValue, targetType);
87-
return wrapAsOptionalIfNecessary(target, typeDescriptor.getType());
88-
}
89-
90-
// From Scalar
91-
92-
if (targetType.isAssignableFrom(rawValue.getClass())) {
93-
return wrapAsOptionalIfNecessary(rawValue, targetType);
94-
}
95-
96-
target = this.argumentInitializer.getTypeConverter().convertIfNecessary(rawValue, targetType);
97-
Assert.state(target != null, () ->
98-
"Cannot convert value type [" + rawValue.getClass() + "] " +
99-
"to argument type [" + targetType.getName() + "].");
100-
return wrapAsOptionalIfNecessary(target, typeDescriptor.getType());
56+
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
57+
return this.argumentInitializer.initializeArgument(environment, name, resolvableType);
10158
}
10259

10360
static String getArgumentName(MethodParameter parameter) {
@@ -115,9 +72,4 @@ static String getArgumentName(MethodParameter parameter) {
11572
"] not specified, and parameter name information not found in class file either.");
11673
}
11774

118-
@Nullable
119-
private Object wrapAsOptionalIfNecessary(@Nullable Object value, Class<?> type) {
120-
return (type.equals(Optional.class) ? Optional.ofNullable(value) : value);
121-
}
122-
12375
}

spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import reactor.core.publisher.Flux;
3131
import reactor.core.publisher.Mono;
3232

33+
import org.springframework.core.ResolvableType;
3334
import org.springframework.data.domain.Example;
3435
import org.springframework.data.domain.Sort;
3536
import org.springframework.data.repository.query.FluentQuery;
@@ -103,8 +104,10 @@ public abstract class QueryByExampleDataFetcher<T> {
103104
* @param env contextual info for the GraphQL query
104105
* @return the resulting example
105106
*/
107+
@SuppressWarnings({"ConstantConditions", "unchecked"})
106108
protected Example<T> buildExample(DataFetchingEnvironment env) {
107-
return Example.of(this.argumentInitializer.initializeFromMap(env.getArguments(), this.domainType.getType()));
109+
ResolvableType targetType = ResolvableType.forClass(this.domainType.getType());
110+
return (Example<T>) Example.of(this.argumentInitializer.initializeArgument(env, null, targetType));
108111
}
109112

110113
protected boolean requiresProjection(Class<?> resultType) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.springframework.core.annotation.SynthesizingMethodParameter;
3434
import org.springframework.format.support.DefaultFormattingConversionService;
3535
import org.springframework.graphql.Book;
36+
import org.springframework.graphql.data.GraphQlArgumentInitializer;
37+
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
3638
import org.springframework.graphql.data.method.annotation.Argument;
3739
import org.springframework.graphql.data.method.annotation.MutationMapping;
3840
import org.springframework.graphql.data.method.annotation.QueryMapping;
@@ -49,7 +51,8 @@ class ArgumentMethodArgumentResolverTests {
4951

5052
private final ObjectMapper mapper = new ObjectMapper();
5153

52-
ArgumentMethodArgumentResolver resolver = new ArgumentMethodArgumentResolver(new DefaultFormattingConversionService());
54+
private final HandlerMethodArgumentResolver resolver =
55+
new ArgumentMethodArgumentResolver(new GraphQlArgumentInitializer(new DefaultFormattingConversionService()));
5356

5457
@Test
5558
void shouldSupportAnnotatedParameters() {

0 commit comments

Comments
 (0)