Skip to content

Commit 3012421

Browse files
ronald-d-rogersapottere
authored andcommitted
Added support for query batching (#48)
* Added support for query batching. * Removed wild-card imports. * Switched back to using input streams where possible. * Streaming output of batched queries. * Removed wild-card imports again.
1 parent 9b8aeea commit 3012421

File tree

3 files changed

+483
-59
lines changed

3 files changed

+483
-59
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# GraphQL Servlet
66

7-
This module implements a GraphQL Java Servlet. It also supports Relay.js and OSGi out of the box.
7+
This module implements a GraphQL Java Servlet. It also supports Relay.js, Apollo and OSGi out of the box.
88

99
# Downloading
1010

@@ -114,6 +114,10 @@ You **MUST** pass this execution strategy to the servlet for Relay.js support.
114114

115115
This is the default execution strategy for the `OsgiGraphQLServlet`, and must be added as a dependency when using that servlet.
116116

117+
## Apollo support
118+
119+
Query batching is supported, no configuration required.
120+
117121
## Spring Framework support
118122

119123
To use the servlet with Spring Framework, either use the [Spring Boot starter](https://github.com/graphql-java/graphql-spring-boot) or simply define a `ServletRegistrationBean` in a web app:

src/main/java/graphql/servlet/GraphQLServlet.java

Lines changed: 189 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package graphql.servlet;
22

3-
import com.fasterxml.jackson.annotation.JacksonInject;
43
import com.fasterxml.jackson.core.JsonParser;
54
import com.fasterxml.jackson.core.type.TypeReference;
65
import com.fasterxml.jackson.databind.DeserializationContext;
@@ -30,13 +29,17 @@
3029
import javax.servlet.http.HttpServlet;
3130
import javax.servlet.http.HttpServletRequest;
3231
import javax.servlet.http.HttpServletResponse;
32+
import java.io.BufferedInputStream;
33+
import java.io.ByteArrayOutputStream;
3334
import java.io.IOException;
3435
import java.io.InputStream;
36+
import java.io.Writer;
3537
import java.security.AccessController;
3638
import java.security.PrivilegedAction;
3739
import java.util.ArrayList;
3840
import java.util.Collections;
3941
import java.util.HashMap;
42+
import java.util.Iterator;
4043
import java.util.List;
4144
import java.util.Map;
4245
import 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

Comments
 (0)