15
15
*/
16
16
package org .springframework .graphql .data .method .annotation .support ;
17
17
18
- import java .io .ByteArrayInputStream ;
19
- import java .io .ByteArrayOutputStream ;
20
- import java .io .IOException ;
21
- import java .io .InputStream ;
22
- import java .io .OutputStream ;
23
- import java .util .Collections ;
24
- import java .util .List ;
18
+ import java .lang .reflect .Constructor ;
19
+ import java .util .Collection ;
20
+ import java .util .Iterator ;
21
+ import java .util .Map ;
25
22
import java .util .Optional ;
23
+ import java .util .Stack ;
26
24
27
25
import graphql .schema .DataFetchingEnvironment ;
28
26
27
+ import org .springframework .beans .BeanUtils ;
28
+ import org .springframework .beans .MutablePropertyValues ;
29
+ import org .springframework .core .CollectionFactory ;
29
30
import org .springframework .core .MethodParameter ;
30
- import org .springframework .core .ResolvableType ;
31
- import org .springframework .core .codec .Decoder ;
32
- import org .springframework .core .codec .Encoder ;
33
- import org .springframework .core .io .buffer .DataBuffer ;
34
- import org .springframework .core .io .buffer .DefaultDataBufferFactory ;
31
+ import org .springframework .core .convert .TypeDescriptor ;
35
32
import org .springframework .graphql .data .method .HandlerMethodArgumentResolver ;
36
33
import org .springframework .graphql .data .method .annotation .Argument ;
37
34
import org .springframework .graphql .data .method .annotation .ValueConstants ;
38
- import org .springframework .http .HttpHeaders ;
39
- import org .springframework .http .HttpInputMessage ;
40
- import org .springframework .http .HttpOutputMessage ;
41
- import org .springframework .http .MediaType ;
42
- import org .springframework .http .converter .GenericHttpMessageConverter ;
43
- import org .springframework .lang .Nullable ;
44
35
import org .springframework .util .Assert ;
45
- import org .springframework .util .MimeTypeUtils ;
46
36
import org .springframework .util .StringUtils ;
37
+ import org .springframework .validation .DataBinder ;
47
38
48
39
/**
49
40
* Resolver for {@link Argument @Argument} annotated method parameters, obtained
50
41
* via {@link DataFetchingEnvironment#getArgument(String)} and converted to the
51
42
* declared type of the method parameter.
52
43
*
53
44
* @author Rossen Stoyanchev
45
+ * @author Brian Clozel
54
46
* @since 1.0.0
55
47
*/
56
48
public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
57
49
58
- private final ArgumentConverter argumentConverter ;
59
-
60
- /**
61
- * Constructor with an
62
- * {@link org.springframework.http.converter.HttpMessageConverter} to convert
63
- * Map-based input arguments to higher level Objects.
64
- */
65
- public ArgumentMethodArgumentResolver (GenericHttpMessageConverter <Object > converter ) {
66
- this .argumentConverter = new MessageConverterArgumentConverter (converter );
67
- }
68
-
69
- /**
70
- * Variant of
71
- * {@link #ArgumentMethodArgumentResolver(GenericHttpMessageConverter)}
72
- * to use an {@link Encoder} and {@link Decoder} to convert input arguments.
73
- */
74
- public ArgumentMethodArgumentResolver (Decoder <Object > decoder , Encoder <Object > encoder ) {
75
- this .argumentConverter = new CodecArgumentConverter (decoder , encoder );
76
- }
77
-
78
-
79
50
@ Override
80
51
public boolean supportsParameter (MethodParameter parameter ) {
81
52
return parameter .getParameterAnnotation (Argument .class ) != null ;
82
53
}
83
54
84
55
@ Override
56
+ @ SuppressWarnings ("unchecked" )
85
57
public Object resolveArgument (MethodParameter parameter , DataFetchingEnvironment environment ) throws Exception {
86
58
Argument annotation = parameter .getParameterAnnotation (Argument .class );
87
59
Assert .notNull (annotation , "No @Argument annotation" );
@@ -99,136 +71,96 @@ public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment
99
71
environment .getArgument (name ) :
100
72
environment .getArgumentOrDefault (name , annotation .defaultValue ()));
101
73
102
- Class <?> parameterType = parameter . getParameterType ( );
74
+ TypeDescriptor parameterType = new TypeDescriptor ( parameter );
103
75
104
76
if (rawValue == null ) {
105
77
if (annotation .required ()) {
106
78
throw new MissingArgumentException (name , parameter );
107
79
}
108
- if (parameterType .equals (Optional .class )) {
80
+ if (parameterType .getType (). equals (Optional .class )) {
109
81
return Optional .empty ();
110
82
}
111
83
return null ;
112
84
}
113
85
114
- if (parameterType .isAssignableFrom (rawValue .getClass ())) {
115
- return returnValue (rawValue , parameterType );
86
+ if (CollectionFactory .isApproximableCollectionType (rawValue .getClass ())) {
87
+ Assert .isAssignable (Collection .class , parameterType .getType (),
88
+ "Argument '" + name + "' is a Collection while the @Argument method parameter is " + parameterType .getType ());
89
+ Collection <Object > rawCollection = (Collection <Object >) rawValue ;
90
+ Collection <Object > values = CollectionFactory .createApproximateCollection (rawValue , rawCollection .size ());
91
+ Class <?> elementType = parameterType .getElementTypeDescriptor ().getType ();
92
+ rawCollection .forEach (item -> values .add (convert (item , elementType )));
93
+ return values ;
116
94
}
117
95
118
- if (rawValue instanceof List ) {
119
- Assert .isAssignable (List .class , parameterType ,
120
- "Argument '" + name + "' is a List while the @Argument method parameter is " + parameterType );
121
- List <?> valueList = (List <?>) rawValue ;
122
- Class <?> elementType = parameter .nestedIfOptional ().getNestedParameterType ();
123
- if (valueList .isEmpty () || elementType .isAssignableFrom (valueList .get (0 ).getClass ())) {
124
- return returnValue (rawValue , parameterType );
125
- }
126
- }
127
-
128
- Object decodedValue = this .argumentConverter .convert (rawValue , parameter );
129
- Assert .notNull (decodedValue , "Argument '" + name + "' with raw value '" + rawValue + "'was decoded to null" );
130
- return returnValue (decodedValue , parameterType );
96
+ MethodParameter nestedParameter = parameter .nestedIfOptional ();
97
+ Object value = convert (rawValue , nestedParameter .getNestedParameterType ());
98
+ return returnValue (value , parameterType .getType ());
131
99
}
132
100
133
101
private Object returnValue (Object value , Class <?> parameterType ) {
134
102
return (parameterType .equals (Optional .class ) ? Optional .of (value ) : value );
135
103
}
136
104
137
-
138
- /**
139
- * Contract to abstract use of an HttpMessageConverter vs Encoder/Decoder.
140
- */
141
- private interface ArgumentConverter {
142
-
143
- @ Nullable
144
- Object convert (Object rawValue , MethodParameter targetParameter ) throws Exception ;
145
-
146
- }
147
-
148
-
149
- /**
150
- * HttpMessageConverter based implementation of ArgumentConverter.
151
- */
152
- private static class MessageConverterArgumentConverter implements ArgumentConverter {
153
-
154
- private final GenericHttpMessageConverter <Object > converter ;
155
-
156
- public MessageConverterArgumentConverter (GenericHttpMessageConverter <Object > converter ) {
157
- this .converter = converter ;
105
+ @ SuppressWarnings ("unchecked" )
106
+ private Object convert (Object rawValue , Class <?> targetType ) {
107
+ Object target ;
108
+ if (rawValue instanceof Map ) {
109
+ Constructor <?> ctor = BeanUtils .getResolvableConstructor (targetType );
110
+ target = BeanUtils .instantiateClass (ctor );
111
+ DataBinder dataBinder = new DataBinder (target );
112
+ Assert .isTrue (ctor .getParameterCount () == 0 ,
113
+ () -> "Argument of type [" + targetType .getName () +
114
+ "] cannot be instantiated because of missing default constructor." );
115
+ MutablePropertyValues mpvs = extractPropertyValues ((Map ) rawValue );
116
+ dataBinder .bind (mpvs );
158
117
}
159
-
160
- @ Override
161
- public Object convert (Object rawValue , MethodParameter targetParameter ) throws IOException {
162
- HttpOutputMessageAdapter outMessage = new HttpOutputMessageAdapter ();
163
- this .converter .write (rawValue , MediaType .APPLICATION_JSON , outMessage );
164
-
165
- HttpInputMessageAdapter inMessage = new HttpInputMessageAdapter (outMessage );
166
- return this .converter .read (targetParameter .getGenericParameterType (), rawValue .getClass (), inMessage );
118
+ else if (targetType .isAssignableFrom (rawValue .getClass ())) {
119
+ return returnValue (rawValue , targetType );
167
120
}
168
- }
169
-
170
-
171
- /**
172
- * Encoder/Decoder based implementation of ArgumentConverter.
173
- */
174
- private static class CodecArgumentConverter implements ArgumentConverter {
175
-
176
- private final Decoder <Object > decoder ;
177
-
178
- private final Encoder <Object > encoder ;
179
-
180
- public CodecArgumentConverter (Decoder <Object > decoder , Encoder <Object > encoder ) {
181
- Assert .notNull (decoder , "Decoder is required" );
182
- Assert .notNull (encoder , "Encoder is required" );
183
- this .decoder = decoder ;
184
- this .encoder = encoder ;
185
- }
186
-
187
- @ Override
188
- public Object convert (Object rawValue , MethodParameter targetParameter ) {
189
- DataBuffer dataBuffer = this .encoder .encodeValue (
190
- rawValue , DefaultDataBufferFactory .sharedInstance , ResolvableType .forInstance (rawValue ),
191
- MimeTypeUtils .APPLICATION_JSON , Collections .emptyMap ());
192
-
193
- return this .decoder .decode (
194
- dataBuffer , ResolvableType .forMethodParameter (targetParameter .nestedIfOptional ()),
195
- MimeTypeUtils .APPLICATION_JSON , Collections .emptyMap ());
121
+ else {
122
+ DataBinder converter = new DataBinder (null );
123
+ target = converter .convertIfNecessary (rawValue , targetType );
124
+ Assert .isTrue (target != null ,
125
+ () -> "Value of type [" + rawValue .getClass () + "] cannot be converted to argument of type [" +
126
+ targetType .getName () + "]." );
196
127
}
128
+ return target ;
197
129
}
198
130
199
-
200
- private static class HttpInputMessageAdapter extends ByteArrayInputStream implements HttpInputMessage {
201
-
202
- HttpInputMessageAdapter (HttpOutputMessageAdapter messageAdapter ) {
203
- super (messageAdapter .toByteArray ());
204
- }
205
-
206
- @ Override
207
- public InputStream getBody () {
208
- return this ;
209
- }
210
-
211
- @ Override
212
- public HttpHeaders getHeaders () {
213
- return HttpHeaders .EMPTY ;
214
- }
215
-
131
+ private MutablePropertyValues extractPropertyValues (Map <String , Object > arguments ) {
132
+ MutablePropertyValues mpvs = new MutablePropertyValues ();
133
+ Stack <String > path = new Stack <>();
134
+ visitArgumentMap (arguments , mpvs , path );
135
+ return mpvs ;
216
136
}
217
137
218
- private static class HttpOutputMessageAdapter extends ByteArrayOutputStream implements HttpOutputMessage {
219
-
220
- private static final HttpHeaders noOpHeaders = new HttpHeaders ();
221
-
222
- @ Override
223
- public OutputStream getBody () {
224
- return this ;
138
+ @ SuppressWarnings ("unchecked" )
139
+ private void visitArgumentMap (Map <String , Object > arguments , MutablePropertyValues mpvs , Stack <String > path ) {
140
+ for (String key : arguments .keySet ()) {
141
+ path .push (key );
142
+ Object value = arguments .get (key );
143
+ if (value instanceof Map ) {
144
+ visitArgumentMap ((Map <String , Object >) value , mpvs , path );
145
+ }
146
+ else {
147
+ String propertyName = pathToPropertyName (path );
148
+ mpvs .add (propertyName , value );
149
+ }
150
+ path .pop ();
225
151
}
152
+ }
226
153
227
- @ Override
228
- public HttpHeaders getHeaders () {
229
- return noOpHeaders ;
154
+ private String pathToPropertyName (Stack <String > path ) {
155
+ StringBuilder sb = new StringBuilder ();
156
+ Iterator <String > it = path .iterator ();
157
+ while (it .hasNext ()) {
158
+ sb .append (it .next ());
159
+ if (it .hasNext ()) {
160
+ sb .append ("." );
161
+ }
230
162
}
231
-
163
+ return sb . toString ();
232
164
}
233
165
234
166
}
0 commit comments