4848
4949
5050/**
51- * Bind a GraphQL argument, or the full arguments map, onto a target object.
51+ * Binder that instantiates and populates a target Object to reflect the
52+ * complete structure of the {@link DataFetchingEnvironment#getArguments()
53+ * GraphQL arguments} input map.
5254 *
53- * <p>Complex objects (non-scalar) are initialized either through the primary
54- * data constructor where arguments are matched to constructor parameters, or
55- * through the default constructor where arguments are matched to setter
56- * property methods. In case objects are related to other objects, binding is
57- * applied recursively to create nested objects.
55+ * <p>The input map is navigated recursively to create the full structure of
56+ * the target type. Objects in the target type are created either through a
57+ * primary, data constructor, in which case arguments are matched to constructor
58+ * parameters by name, or through the default constructor, in which case
59+ * arguments are matched to properties. Scalar values are converted, if
60+ * necessary, through a {@link ConversionService}.
5861 *
59- * <p>Scalar values are converted to the expected target type through a
60- * {@link ConversionService}, if provided.
62+ * <p>The binder does not stop at the first error, but rather accumulates as
63+ * many errors as it can in a {@link org.springframework.validation.BindingResult}.
64+ * At the end it raises a {@link BindException} that contains all recorded
65+ * errors along with the path at which each error occurred.
6166 *
62- * <p>In case of any errors, when creating objects or converting scalar values,
63- * a {@link BindException} is raised that contains all errors recorded along
64- * with the path at which the errors occurred.
67+ * <p>The binder supports {@link Optional} as a wrapper around any Object or
68+ * scalar value in the target Object structure. In addition, it also supports
69+ * {@link ArgumentValue} as a wrapper that indicates whether a given input
70+ * argument was omitted rather than set to the {@literal "null"} literal.
6571 *
6672 * @author Brian Clozel
6773 * @author Rossen Stoyanchev
@@ -103,18 +109,15 @@ public void addDataBinderInitializer(Consumer<DataBinder> consumer) {
103109
104110
105111 /**
106- * Bind a single argument, or the full arguments map, onto an object of the
107- * given target type .
112+ * Create and populate an Object of the given target type, from a single
113+ * GraphQL argument, or from the full GraphQL arguments map .
108114 * @param environment for access to the arguments
109- * @param name the name of the argument to bind, or {@code null} to
110- * use the full arguments map
115+ * @param name the name of an argument, or {@code null} to use the full map
111116 * @param targetType the type of Object to create
112- * @return the created Object, possibly {@code null}
113- * @throws BindException in case of binding issues such as conversion errors,
114- * mismatches between the source and the target object structure, and so on.
115- * Binding issues are accumulated as {@link BindException#getFieldErrors()
116- * field errors} where the {@link FieldError#getField() field} of each error
117- * is the argument path where the issue occurred.
117+ * @return the created Object, possibly wrapped in {@link Optional} or in
118+ * {@link ArgumentValue}, or {@code null} if there is no value
119+ * @throws BindException containing one or more accumulated errors from
120+ * matching and/or converting arguments to the target Object
118121 */
119122 @ Nullable
120123 public Object bind (
@@ -137,21 +140,21 @@ public Object bind(
137140 }
138141
139142 /**
140- * Bind the raw GraphQL argument value to an Object of the specified type .
141- * @param name the name of a constructor parameter or a bean property of the
142- * target Object that is to be initialized from the given raw value;
143- * {@code "$"} if binding the top level Object; possibly indexed if binding
144- * to a Collection element or to a Map value.
143+ * Create an Object from the given raw GraphQL argument value.
144+ * @param name the name of the constructor parameter or the property that
145+ * will be set from the returned value, possibly {@code "$"} for the top
146+ * Object, or an indexed property for a Collection element or Map value;
147+ * mainly for error recording, to keep track of the nested path
145148 * @param rawValue the raw argument value (Collection, Map, or scalar)
146- * @param isOmitted whether the value with the given name was not provided
147- * at all, as opposed to provided but set to the {@literal " null"} literal
149+ * @param isOmitted {@code true} if the argument was omitted from the input
150+ * and {@code false} if it was provided, but possibly {@code null}
148151 * @param targetType the type of Object to create
149- * @param targetClass the resolved class from the targetType
150- * @param bindingResult for keeping track of the nested path and errors
152+ * @param targetClass the target class, resolved from the targetType
153+ * @param bindingResult to accumulate errors
151154 * @return the target Object instance, possibly {@code null} if the source
152- * value is {@code null} or if binding failed in which case the result will
153- * contain errors; nevertheless we keep going to record as many errors as
154- * we can accumulate
155+ * value is {@code null}, or if binding failed in which case the result will
156+ * contain errors; generally we keep going as far as we can and only raise
157+ * a {@link BindException} at the end to record as many errors as possible
155158 */
156159 @ SuppressWarnings ({"ConstantConditions" , "unchecked" })
157160 @ Nullable
@@ -199,8 +202,8 @@ private Collection<?> bindCollection(
199202 ResolvableType elementType = collectionType .asCollection ().getGeneric (0 );
200203 Class <?> elementClass = collectionType .asCollection ().getGeneric (0 ).resolve ();
201204 if (elementClass == null ) {
202- bindingResult .rejectValue ( null , "unknownType" , "Unknown Collection element type" );
203- return Collections .emptyList (); // Keep going, report as many errors as we can
205+ bindingResult .rejectArgumentValue ( name , null , "unknownType" , "Unknown Collection element type" );
206+ return Collections .emptyList (); // Keep going, to record more errors
204207 }
205208
206209 Collection <Object > collection =
@@ -228,9 +231,9 @@ private Object bindMap(
228231
229232 Constructor <?> constructor = BeanUtils .getResolvableConstructor (targetClass );
230233
231- Object value = constructor .getParameterCount () > 0 ?
234+ Object value = ( constructor .getParameterCount () > 0 ?
232235 bindMapToObjectViaConstructor (rawMap , constructor , targetType , bindingResult ) :
233- bindMapToObjectViaSetters (rawMap , constructor , targetType , bindingResult );
236+ bindMapToObjectViaSetters (rawMap , constructor , targetType , bindingResult )) ;
234237
235238 bindingResult .popNestedPath ();
236239
@@ -244,8 +247,8 @@ private Map<?, Object> bindMapToMap(
244247 ResolvableType valueType = targetType .asMap ().getGeneric (1 );
245248 Class <?> valueClass = valueType .resolve ();
246249 if (valueClass == null ) {
247- bindingResult .rejectValue ( null , "unknownType" , "Unknown Map value type" );
248- return Collections .emptyMap (); // Keep going, report as many errors as we can
250+ bindingResult .rejectArgumentValue ( name , null , "unknownType" , "Unknown Map value type" );
251+ return Collections .emptyMap (); // Keep going, to record more errors
249252 }
250253
251254 Map <String , Object > map = CollectionFactory .createMap (targetClass , rawMap .size ());
@@ -261,28 +264,28 @@ private Map<?, Object> bindMapToMap(
261264
262265 @ Nullable
263266 private Object bindMapToObjectViaConstructor (
264- Map <String , Object > rawMap , Constructor <?> constructor , ResolvableType parentType ,
267+ Map <String , Object > rawMap , Constructor <?> constructor , ResolvableType ownerType ,
265268 ArgumentsBindingResult bindingResult ) {
266269
267270 String [] paramNames = BeanUtils .getParameterNames (constructor );
268271 Class <?>[] paramTypes = constructor .getParameterTypes ();
269- Object [] args = new Object [paramTypes .length ];
272+ Object [] constructorArguments = new Object [paramTypes .length ];
270273
271274 for (int i = 0 ; i < paramNames .length ; i ++) {
272275 String name = paramNames [i ];
273- boolean isOmitted = !rawMap .containsKey (name );
274276
275277 ResolvableType targetType = ResolvableType .forType (
276- ResolvableType .forConstructorParameter (constructor , i ).getType (), parentType );
278+ ResolvableType .forConstructorParameter (constructor , i ).getType (), ownerType );
277279
278- args [i ] = bindRawValue (name , rawMap .get (name ), isOmitted , targetType , paramTypes [i ], bindingResult );
280+ constructorArguments [i ] = bindRawValue (
281+ name , rawMap .get (name ), !rawMap .containsKey (name ), targetType , paramTypes [i ], bindingResult );
279282 }
280283
281284 try {
282- return BeanUtils .instantiateClass (constructor , args );
285+ return BeanUtils .instantiateClass (constructor , constructorArguments );
283286 }
284287 catch (BeanInstantiationException ex ) {
285- // Ignore: we had binding errors to begin with
288+ // Ignore, if we had binding errors to begin with
286289 if (bindingResult .hasErrors ()) {
287290 return null ;
288291 }
@@ -291,7 +294,7 @@ private Object bindMapToObjectViaConstructor(
291294 }
292295
293296 private Object bindMapToObjectViaSetters (
294- Map <String , Object > rawMap , Constructor <?> constructor , ResolvableType parentType ,
297+ Map <String , Object > rawMap , Constructor <?> constructor , ResolvableType ownerType ,
295298 ArgumentsBindingResult bindingResult ) {
296299
297300 Object target = BeanUtils .instantiateClass (constructor );
@@ -306,7 +309,7 @@ private Object bindMapToObjectViaSetters(
306309 }
307310
308311 ResolvableType targetType =
309- ResolvableType .forType (typeDescriptor .getResolvableType ().getType (), parentType );
312+ ResolvableType .forType (typeDescriptor .getResolvableType ().getType (), ownerType );
310313
311314 Object value = bindRawValue (
312315 key , entry .getValue (), false , targetType , typeDescriptor .getType (), bindingResult );
@@ -320,7 +323,7 @@ private Object bindMapToObjectViaSetters(
320323 // Ignore unknown property
321324 }
322325 catch (Exception ex ) {
323- bindingResult .rejectValue ( value , "invalidPropertyValue" , "Failed to set property value" );
326+ bindingResult .rejectArgumentValue ( key , value , "invalidPropertyValue" , "Failed to set property value" );
324327 }
325328 }
326329
@@ -339,22 +342,19 @@ private <T> T convertValue(
339342 (this .typeConverter != null ? this .typeConverter : new SimpleTypeConverter ());
340343
341344 value = converter .convertIfNecessary (
342- rawValue , (Class <?>) clazz ,
343- (type .getSource () instanceof MethodParameter param ? new TypeDescriptor (param ) : null ));
345+ rawValue , (Class <?>) clazz , new TypeDescriptor (type , null , null ));
344346 }
345347 catch (TypeMismatchException ex ) {
346- bindingResult .pushNestedPath (name );
347- bindingResult .rejectValue (rawValue , ex .getErrorCode (), "Failed to convert argument value" );
348- bindingResult .popNestedPath ();
348+ bindingResult .rejectArgumentValue (name , rawValue , ex .getErrorCode (), "Failed to convert argument value" );
349349 }
350350
351351 return (T ) value ;
352352 }
353353
354354
355355 /**
356- * BindingResult without a target Object, only for keeping track of errors
357- * and their associated, nested paths .
356+ * Subclass of {@link AbstractBindingResult} that doesn't have a target Object,
357+ * and takes the raw value as input when recording errors .
358358 */
359359 @ SuppressWarnings ("serial" )
360360 private static class ArgumentsBindingResult extends AbstractBindingResult {
@@ -379,9 +379,11 @@ protected Object getActualFieldValue(String field) {
379379 return null ;
380380 }
381381
382- public void rejectValue (@ Nullable Object rawValue , String code , String defaultMessage ) {
382+ public void rejectArgumentValue (
383+ String field , @ Nullable Object rawValue , String code , String defaultMessage ) {
384+
383385 addError (new FieldError (
384- getObjectName (), fixedField (null ), rawValue , true , resolveMessageCodes (code ),
386+ getObjectName (), fixedField (field ), rawValue , true , resolveMessageCodes (code ),
385387 null , defaultMessage ));
386388 }
387389 }
0 commit comments