11package graphql .servlet ;
22
3- import com .fasterxml .jackson .annotation .JacksonInject ;
43import com .fasterxml .jackson .core .JsonParser ;
54import com .fasterxml .jackson .core .type .TypeReference ;
65import com .fasterxml .jackson .databind .DeserializationContext ;
3029import javax .servlet .http .HttpServlet ;
3130import javax .servlet .http .HttpServletRequest ;
3231import javax .servlet .http .HttpServletResponse ;
32+ import java .io .BufferedInputStream ;
33+ import java .io .ByteArrayOutputStream ;
3334import java .io .IOException ;
3435import java .io .InputStream ;
36+ import java .io .Writer ;
3537import java .security .AccessController ;
3638import java .security .PrivilegedAction ;
3739import java .util .ArrayList ;
3840import java .util .Collections ;
3941import java .util .HashMap ;
42+ import java .util .Iterator ;
4043import java .util .List ;
4144import java .util .Map ;
4245import java .util .Objects ;
@@ -69,8 +72,8 @@ public abstract class GraphQLServlet extends HttpServlet implements Servlet, Gra
6972 private final List <GraphQLServletListener > listeners ;
7073 private final ServletFileUpload fileUpload ;
7174
72- private final RequestHandler getHandler ;
73- private final RequestHandler postHandler ;
75+ private final HttpRequestHandler getHandler ;
76+ private final HttpRequestHandler postHandler ;
7477
7578 public GraphQLServlet () {
7679 this (null , null , null );
@@ -84,23 +87,31 @@ public GraphQLServlet(ObjectMapperConfigurer objectMapperConfigurer, List<GraphQ
8487 this .getHandler = (request , response ) -> {
8588 final GraphQLContext context = createContext (Optional .of (request ), Optional .of (response ));
8689 final Object rootObject = createRootObject (Optional .of (request ), Optional .of (response ));
90+
8791 String path = request .getPathInfo ();
8892 if (path == null ) {
8993 path = request .getServletPath ();
9094 }
9195 if (path .contentEquals ("/schema.json" )) {
92- query (IntrospectionQuery .INTROSPECTION_QUERY , null , new HashMap <>(), getSchemaProvider ().getSchema (request ), request , response , context , rootObject );
96+ doQuery (IntrospectionQuery .INTROSPECTION_QUERY , null , new HashMap <>(), getSchemaProvider ().getSchema (request ), context , rootObject , request , response );
9397 } else {
94- if (request .getParameter ("query" ) != null ) {
95- final Map <String , Object > variables = new HashMap <>();
96- if (request .getParameter ("variables" ) != null ) {
97- variables .putAll (deserializeVariables (request .getParameter ("variables" )));
98- }
99- String operationName = null ;
100- if (request .getParameter ("operationName" ) != null ) {
101- operationName = request .getParameter ("operationName" );
98+ String query = request .getParameter ("query" );
99+ if (query != null ) {
100+ if (isBatchedQuery (query )) {
101+ doBatchedQuery (getGraphQLRequestMapper ().readValues (query ), getSchemaProvider ().getReadOnlySchema (request ), context , rootObject , request , response );
102+ } else {
103+ final Map <String , Object > variables = new HashMap <>();
104+ if (request .getParameter ("variables" ) != null ) {
105+ variables .putAll (deserializeVariables (request .getParameter ("variables" )));
106+ }
107+
108+ String operationName = null ;
109+ if (request .getParameter ("operationName" ) != null ) {
110+ operationName = request .getParameter ("operationName" );
111+ }
112+
113+ doQuery (query , operationName , variables , getSchemaProvider ().getReadOnlySchema (request ), context , rootObject , request , response );
102114 }
103- query (request .getParameter ("query" ), operationName , variables , getSchemaProvider ().getReadOnlySchema (request ), request , response , context , rootObject );
104115 } else {
105116 response .setStatus (STATUS_BAD_REQUEST );
106117 log .info ("Bad GET request: path was not \" /schema.json\" or no query variable named \" query\" given" );
@@ -111,70 +122,82 @@ public GraphQLServlet(ObjectMapperConfigurer objectMapperConfigurer, List<GraphQ
111122 this .postHandler = (request , response ) -> {
112123 final GraphQLContext context = createContext (Optional .of (request ), Optional .of (response ));
113124 final Object rootObject = createRootObject (Optional .of (request ), Optional .of (response ));
114- GraphQLRequest graphQLRequest = null ;
115125
116126 try {
117- InputStream inputStream = null ;
118-
119127 if (ServletFileUpload .isMultipartContent (request )) {
120128 final Map <String , List <FileItem >> fileItems = fileUpload .parseParameterMap (request );
129+ context .setFiles (Optional .of (fileItems ));
121130
122131 if (fileItems .containsKey ("graphql" )) {
123132 final Optional <FileItem > graphqlItem = getFileItem (fileItems , "graphql" );
124133 if (graphqlItem .isPresent ()) {
125- inputStream = graphqlItem .get ().getInputStream ();
126- }
134+ InputStream inputStream = graphqlItem .get ().getInputStream ();
127135
136+ if (!inputStream .markSupported ()) {
137+ inputStream = new BufferedInputStream (inputStream );
138+ }
139+
140+ if (isBatchedQuery (inputStream )) {
141+ doBatchedQuery (getGraphQLRequestMapper ().readValues (inputStream ), getSchemaProvider ().getSchema (request ), context , rootObject , request , response );
142+ return ;
143+ } else {
144+ doQuery (getGraphQLRequestMapper ().readValue (inputStream ), getSchemaProvider ().getSchema (request ), context , rootObject , request , response );
145+ return ;
146+ }
147+ }
128148 } else if (fileItems .containsKey ("query" )) {
129149 final Optional <FileItem > queryItem = getFileItem (fileItems , "query" );
130150 if (queryItem .isPresent ()) {
131- graphQLRequest = new GraphQLRequest ();
132- graphQLRequest .setQuery (new String (queryItem .get ().get ()));
151+ InputStream inputStream = queryItem .get ().getInputStream ();
133152
134- final Optional <FileItem > operationNameItem = getFileItem (fileItems , "operationName" );
135- if (operationNameItem .isPresent ()) {
136- graphQLRequest .setOperationName (new String (operationNameItem .get ().get ()).trim ());
153+ if (!inputStream .markSupported ()) {
154+ inputStream = new BufferedInputStream (inputStream );
137155 }
138156
139- final Optional <FileItem > variablesItem = getFileItem (fileItems , "variables" );
140- if (variablesItem .isPresent ()) {
141- String variables = new String (variablesItem .get ().get ());
142- if (!variables .isEmpty ()) {
143- graphQLRequest .setVariables (deserializeVariables (variables ));
157+ if (isBatchedQuery (inputStream )) {
158+ doBatchedQuery (getGraphQLRequestMapper ().readValues (inputStream ), getSchemaProvider ().getSchema (request ), context , rootObject , request , response );
159+ return ;
160+ } else {
161+ String query = new String (queryItem .get ().get ());
162+
163+ Map <String , Object > variables = null ;
164+ final Optional <FileItem > variablesItem = getFileItem (fileItems , "variables" );
165+ if (variablesItem .isPresent ()) {
166+ variables = deserializeVariables (new String (variablesItem .get ().get ()));
144167 }
168+
169+ String operationName = null ;
170+ final Optional <FileItem > operationNameItem = getFileItem (fileItems , "operationName" );
171+ if (operationNameItem .isPresent ()) {
172+ operationName = new String (operationNameItem .get ().get ()).trim ();
173+ }
174+
175+ doQuery (query , operationName , variables , getSchemaProvider ().getSchema (request ), context , rootObject , request , response );
176+ return ;
145177 }
146178 }
147179 }
148180
149- if (inputStream == null && graphQLRequest == null ) {
150- response .setStatus (STATUS_BAD_REQUEST );
151- log .info ("Bad POST multipart request: no part named \" graphql\" or \" query\" " );
152- return ;
153- }
154-
155- context .setFiles (Optional .of (fileItems ));
156-
181+ response .setStatus (STATUS_BAD_REQUEST );
182+ log .info ("Bad POST multipart request: no part named \" graphql\" or \" query\" " );
157183 } else {
158184 // this is not a multipart request
159- inputStream = request .getInputStream ();
160- }
185+ InputStream inputStream = request .getInputStream ();
161186
162- if ( graphQLRequest == null ) {
163- graphQLRequest = getGraphQLRequestMapper (). readValue (inputStream );
164- }
187+ if (! inputStream . markSupported () ) {
188+ inputStream = new BufferedInputStream (inputStream );
189+ }
165190
191+ if (isBatchedQuery (inputStream )) {
192+ doBatchedQuery (getGraphQLRequestMapper ().readValues (inputStream ), getSchemaProvider ().getSchema (request ), context , rootObject , request , response );
193+ } else {
194+ doQuery (getGraphQLRequestMapper ().readValue (inputStream ), getSchemaProvider ().getSchema (request ), context , rootObject , request , response );
195+ }
196+ }
166197 } catch (Exception e ) {
167198 log .info ("Bad POST request: parsing failed" , e );
168199 response .setStatus (STATUS_BAD_REQUEST );
169- return ;
170- }
171-
172- Map <String ,Object > variables = graphQLRequest .getVariables ();
173- if (variables == null ) {
174- variables = new HashMap <>();
175200 }
176-
177- query (graphQLRequest .getQuery (), graphQLRequest .getOperationName (), variables , getSchemaProvider ().getSchema (request ), request , response , context , rootObject );
178201 };
179202 }
180203
@@ -221,7 +244,7 @@ public String executeQuery(String query) {
221244 }
222245 }
223246
224- private void doRequest (HttpServletRequest request , HttpServletResponse response , RequestHandler handler ) {
247+ private void doRequest (HttpServletRequest request , HttpServletResponse response , HttpRequestHandler handler ) {
225248
226249 List <GraphQLServletListener .RequestCallback > requestCallbacks = runListeners (l -> l .onRequest (request , response ));
227250
@@ -266,14 +289,42 @@ private GraphQL newGraphQL(GraphQLSchema schema) {
266289 .build ();
267290 }
268291
269- private void query (String query , String operationName , Map <String , Object > variables , GraphQLSchema schema , HttpServletRequest req , HttpServletResponse resp , GraphQLContext context , Object rootObject ) throws IOException {
292+ private void doQuery (GraphQLRequest graphQLRequest , GraphQLSchema schema , GraphQLContext context , Object rootObject , HttpServletRequest httpReq , HttpServletResponse httpRes ) throws Exception {
293+ doQuery (graphQLRequest .getQuery (), graphQLRequest .getOperationName (), graphQLRequest .getVariables (), schema , context , rootObject , httpReq , httpRes );
294+ }
295+
296+ private void doQuery (String query , String operationName , Map <String , Object > variables , GraphQLSchema schema , GraphQLContext context , Object rootObject , HttpServletRequest req , HttpServletResponse resp ) throws Exception {
297+ query (query , operationName , variables , schema , context , rootObject , (r ) -> {
298+ resp .setContentType (APPLICATION_JSON_UTF8 );
299+ resp .setStatus (r .getStatus ());
300+ resp .getWriter ().write (r .getResponse ());
301+ });
302+ }
303+
304+ private void doBatchedQuery (Iterator <GraphQLRequest > graphQLRequests , GraphQLSchema schema , GraphQLContext context , Object rootObject , HttpServletRequest req , HttpServletResponse resp ) throws Exception {
305+ resp .setContentType (APPLICATION_JSON_UTF8 );
306+ resp .setStatus (STATUS_OK );
307+
308+ Writer respWriter = resp .getWriter ();
309+ respWriter .write ('[' );
310+ while (graphQLRequests .hasNext ()) {
311+ GraphQLRequest graphQLRequest = graphQLRequests .next ();
312+ query (graphQLRequest .getQuery (), graphQLRequest .getOperationName (), graphQLRequest .getVariables (), schema , context , rootObject , (r ) -> respWriter .write (r .getResponse ()));
313+ if (graphQLRequests .hasNext ()) {
314+ respWriter .write (',' );
315+ }
316+ }
317+ respWriter .write (']' );
318+ }
319+
320+ private void query (String query , String operationName , Map <String , Object > variables , GraphQLSchema schema , GraphQLContext context , Object rootObject , GraphQLResponseHandler responseHandler ) throws Exception {
270321 if (operationName != null && operationName .isEmpty ()) {
271- query (query , null , variables , schema , req , resp , context , rootObject );
322+ query (query , null , variables , schema , context , rootObject , responseHandler );
272323 } else if (Subject .getSubject (AccessController .getContext ()) == null && context .getSubject ().isPresent ()) {
273324 Subject .doAs (context .getSubject ().get (), (PrivilegedAction <Void >) () -> {
274325 try {
275- query (query , operationName , variables , schema , req , resp , context , rootObject );
276- } catch (IOException e ) {
326+ query (query , operationName , variables , schema , context , rootObject , responseHandler );
327+ } catch (Exception e ) {
277328 throw new RuntimeException (e );
278329 }
279330 return null ;
@@ -287,9 +338,10 @@ private void query(String query, String operationName, Map<String, Object> varia
287338
288339 final String response = getMapper ().writeValueAsString (createResultFromDataAndErrors (data , errors ));
289340
290- resp .setContentType (APPLICATION_JSON_UTF8 );
291- resp .setStatus (STATUS_OK );
292- resp .getWriter ().write (response );
341+ GraphQLResponse graphQLResponse = new GraphQLResponse ();
342+ graphQLResponse .setStatus (STATUS_OK );
343+ graphQLResponse .setResponse (response );
344+ responseHandler .handle (graphQLResponse );
293345
294346 if (getGraphQLErrorHandler ().errorsPresent (errors )) {
295347 runCallbacks (operationCallbacks , c -> c .onError (context , operationName , query , variables , data , errors ));
@@ -373,6 +425,51 @@ private static Map<String, Object> deserializeVariablesObject(Object variables,
373425 }
374426 }
375427
428+ private boolean isBatchedQuery (InputStream inputStream ) throws IOException {
429+ if (inputStream == null ) {
430+ return false ;
431+ }
432+
433+ ByteArrayOutputStream result = new ByteArrayOutputStream ();
434+ byte [] buffer = new byte [128 ];
435+ int length ;
436+
437+ inputStream .mark (0 );
438+ while ((length = inputStream .read (buffer )) != -1 ) {
439+ result .write (buffer , 0 , length );
440+ String chunk = result .toString ();
441+ Boolean isArrayStart = isArrayStart (chunk );
442+ if (isArrayStart != null ) {
443+ inputStream .reset ();
444+ return isArrayStart ;
445+ }
446+ }
447+
448+ inputStream .reset ();
449+ return false ;
450+ }
451+
452+ private boolean isBatchedQuery (String query ) {
453+ if (query == null ) {
454+ return false ;
455+ }
456+
457+ Boolean isArrayStart = isArrayStart (query );
458+ return isArrayStart != null && isArrayStart ;
459+ }
460+
461+ // return true if the first non whitespace character is the beginning of an array
462+ private Boolean isArrayStart (String s ) {
463+ for (int i = 0 ; i < s .length (); i ++) {
464+ char ch = s .charAt (i );
465+ if (!Character .isWhitespace (ch )) {
466+ return ch == '[' ;
467+ }
468+ }
469+
470+ return null ;
471+ }
472+
376473 protected static class GraphQLRequest {
377474 private String query ;
378475 @ JsonDeserialize (using = GraphQLServlet .VariablesDeserializer .class )
@@ -404,7 +501,28 @@ public void setOperationName(String operationName) {
404501 }
405502 }
406503
407- protected interface RequestHandler extends BiConsumer <HttpServletRequest , HttpServletResponse > {
504+ protected static class GraphQLResponse {
505+ private int status ;
506+ private String response ;
507+
508+ public int getStatus () {
509+ return status ;
510+ }
511+
512+ public void setStatus (int status ) {
513+ this .status = status ;
514+ }
515+
516+ public String getResponse () {
517+ return response ;
518+ }
519+
520+ public void setResponse (String response ) {
521+ this .response = response ;
522+ }
523+ }
524+
525+ protected interface HttpRequestHandler extends BiConsumer <HttpServletRequest , HttpServletResponse > {
408526 @ Override
409527 default void accept (HttpServletRequest request , HttpServletResponse response ) {
410528 try {
@@ -416,4 +534,17 @@ default void accept(HttpServletRequest request, HttpServletResponse response) {
416534
417535 void handle (HttpServletRequest request , HttpServletResponse response ) throws Exception ;
418536 }
537+
538+ protected interface GraphQLResponseHandler extends Consumer <GraphQLResponse > {
539+ @ Override
540+ default void accept (GraphQLResponse response ) {
541+ try {
542+ handle (response );
543+ } catch (Exception e ) {
544+ throw new RuntimeException (e );
545+ }
546+ }
547+
548+ void handle (GraphQLResponse r ) throws Exception ;
549+ }
419550}
0 commit comments