Skip to content

Commit f15b174

Browse files
committed
a simple example of using http with graphql-java
1 parent 9addb02 commit f15b174

7 files changed

Lines changed: 450 additions & 0 deletions

File tree

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ dependencies {
3535
testCompile 'org.codehaus.groovy:groovy-all:2.4.10'
3636
testCompile 'cglib:cglib-nodep:3.1'
3737
testCompile 'org.objenesis:objenesis:2.1'
38+
testCompile 'com.google.code.gson:gson:2.8.0'
39+
testCompile 'org.eclipse.jetty:jetty-server:9.4.5.v20170502'
40+
testCompile 'org.slf4j:slf4j-simple:1.7.22'
3841
}
3942

4043
compileJava.source file("build/generated-src"), sourceSets.main.java
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package example.http;
2+
3+
import javax.servlet.http.HttpServletRequest;
4+
import java.io.UnsupportedEncodingException;
5+
import java.net.URLDecoder;
6+
import java.util.Optional;
7+
8+
public class HttpKit {
9+
/**
10+
* We should not have to write this method but to get the query string ONLY parameter
11+
* in Http Servlet spec world means parsing the query string on POST
12+
*
13+
* @param request the http servlet request
14+
* @param parameterName the name of the parameter
15+
*
16+
* @return an optional parameter
17+
*/
18+
public static Optional<String> getQueryParameter(HttpServletRequest request, String parameterName) {
19+
if ("POST".equalsIgnoreCase(request.getMethod())) {
20+
parameterName = parameterName + "=";
21+
String queryString = request.getQueryString();
22+
if (queryString == null) {
23+
return Optional.empty();
24+
}
25+
int index = queryString.indexOf(parameterName);
26+
if (index == -1) {
27+
return Optional.empty();
28+
}
29+
StringBuilder sb = new StringBuilder();
30+
index += parameterName.length();
31+
while (index < queryString.length()) {
32+
char c = queryString.charAt(index);
33+
if (c == '&') {
34+
break;
35+
}
36+
sb.append(c);
37+
index++;
38+
}
39+
try {
40+
return Optional.of(URLDecoder.decode(sb.toString(), "UTF-8"));
41+
} catch (UnsupportedEncodingException e) {
42+
throw new RuntimeException(e);
43+
}
44+
} else {
45+
return Optional.ofNullable(request.getParameter(parameterName));
46+
}
47+
}
48+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package example.http;
2+
3+
import graphql.ExecutionResult;
4+
import graphql.GraphQL;
5+
import graphql.StarWarsData;
6+
import graphql.schema.GraphQLSchema;
7+
import graphql.schema.idl.RuntimeWiring;
8+
import graphql.schema.idl.SchemaGenerator;
9+
import graphql.schema.idl.SchemaParser;
10+
import graphql.schema.idl.TypeDefinitionRegistry;
11+
import org.eclipse.jetty.server.Request;
12+
import org.eclipse.jetty.server.Server;
13+
import org.eclipse.jetty.server.handler.AbstractHandler;
14+
15+
import javax.servlet.ServletException;
16+
import javax.servlet.http.HttpServletRequest;
17+
import javax.servlet.http.HttpServletResponse;
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.io.InputStreamReader;
21+
import java.io.Reader;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
25+
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
26+
27+
/**
28+
* An very simple example of serving a qraphql schema over http.
29+
*
30+
* More info can be found here : http://graphql.org/learn/serving-over-http/
31+
*/
32+
public class HttpMain extends AbstractHandler {
33+
34+
static final int PORT = 3000;
35+
static GraphQLSchema starWarsSchema = null;
36+
37+
public static void main(String[] args) throws Exception {
38+
//
39+
// This example uses Jetty as an embedded HTTP server
40+
Server server = new Server(PORT);
41+
//
42+
// In Jetty, handlers are how your get called backed on a request
43+
server.setHandler(new HttpMain());
44+
server.start();
45+
46+
server.join();
47+
}
48+
49+
@Override
50+
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
51+
if ("/graphql".equals(target) || "/".equals(target)) {
52+
handleStarWars(request, response);
53+
}
54+
baseRequest.setHandled(true);
55+
}
56+
57+
private void handleStarWars(HttpServletRequest request, HttpServletResponse response) throws IOException {
58+
//
59+
// this builds out the parameters we need like the graphql query
60+
QueryParameters parameters = QueryParameters.from(request);
61+
if (parameters.getQuery() == null) {
62+
response.setStatus(400);
63+
return;
64+
}
65+
66+
//
67+
// the context object is something that means something to down stream code. It is instructions
68+
// from yourself to your other code such as DataFetchers. The engine passes this on unchanged and
69+
// makes it available to inner code
70+
//
71+
// the graphql guidance says :
72+
//
73+
// - GraphQL should be placed after all authentication middleware, so that you
74+
// - have access to the same session and user information you would in your
75+
// - HTTP endpoint handlers.
76+
//
77+
Map<String, Object> context = new HashMap<>();
78+
context.put("YouAppSecurityClearanceLevel", "CodeRed");
79+
context.put("YouAppExecutingUser", "Dr Nefarious");
80+
81+
//
82+
// you need a schema in order to execute queries
83+
GraphQLSchema schema = buildStarWarsSchema();
84+
85+
// finally you build a runtime graphql object and execute the query
86+
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
87+
ExecutionResult executionResult = graphQL.execute(
88+
parameters.getQuery(),
89+
parameters.getOperationName(),
90+
context,
91+
parameters.getVariables()
92+
);
93+
94+
returnAsJson(response, executionResult);
95+
}
96+
97+
98+
private void returnAsJson(HttpServletResponse response, ExecutionResult executionResult) throws IOException {
99+
response.setContentType("application/json");
100+
response.setStatus(HttpServletResponse.SC_OK);
101+
JsonKit.toJson(response, executionResult);
102+
}
103+
104+
private GraphQLSchema buildStarWarsSchema() {
105+
//
106+
// using lazy loading here ensure we can debug the schema generation
107+
// and potentially get "wired" components that cant be accessed
108+
// statically.
109+
//
110+
// A full application would use a dependency injection framework (like Spring)
111+
// to manage that lifecycle.
112+
//
113+
if (starWarsSchema == null) {
114+
115+
//
116+
// reads a file that provides the schema types
117+
//
118+
Reader streamReader = loadSchemaFile("starWarsSchemaAnnotated.graphqls");
119+
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(streamReader);
120+
121+
//
122+
// the runtime wiring is used to provide the code that backs the
123+
// logical schema
124+
//
125+
RuntimeWiring wiring = RuntimeWiring.newRuntimeWiring()
126+
.type(newTypeWiring("Query")
127+
.dataFetcher("hero", StarWarsData.getHeroDataFetcher())
128+
.dataFetcher("human", StarWarsData.getHumanDataFetcher())
129+
.dataFetcher("droid", StarWarsData.getDroidDataFetcher())
130+
)
131+
.type(newTypeWiring("Human")
132+
.dataFetcher("friends", StarWarsData.getFriendsDataFetcher())
133+
)
134+
.type(newTypeWiring("Droid")
135+
.dataFetcher("friends", StarWarsData.getFriendsDataFetcher())
136+
)
137+
138+
.type(newTypeWiring("Character")
139+
.typeResolver(StarWarsData.getCharacterTypeResolver())
140+
)
141+
.type(newTypeWiring("Episode")
142+
.enumValues(StarWarsData.getEpisodeResolver())
143+
)
144+
.build();
145+
146+
// finally combine the logical schema with the physical runtime
147+
starWarsSchema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);
148+
}
149+
return starWarsSchema;
150+
}
151+
152+
private Reader loadSchemaFile(String name) {
153+
InputStream stream = getClass().getClassLoader().getResourceAsStream(name);
154+
return new InputStreamReader(stream);
155+
}
156+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package example.http;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.GsonBuilder;
5+
import com.google.gson.reflect.TypeToken;
6+
import graphql.ExecutionResult;
7+
8+
import javax.servlet.http.HttpServletResponse;
9+
import java.io.IOException;
10+
import java.util.Collections;
11+
import java.util.Map;
12+
13+
/**
14+
* This example code chose to use GSON as its JSON parser. Any JSON parser should be fine
15+
*/
16+
public class JsonKit {
17+
static final Gson GSON = new GsonBuilder()
18+
.serializeNulls()
19+
.create();
20+
21+
public static void toJson(HttpServletResponse response, ExecutionResult executionResult) throws IOException {
22+
GSON.toJson(executionResult, response.getWriter());
23+
}
24+
25+
public static Map<String, Object> toMap(String jsonStr) {
26+
if (jsonStr == null || jsonStr.trim().length() == 0) {
27+
return Collections.emptyMap();
28+
}
29+
// gson uses type tokens for generic input like Map<String,Object>
30+
TypeToken<Map<String, Object>> typeToken = new TypeToken<Map<String, Object>>() {
31+
};
32+
Map<String, Object> map = GSON.fromJson(jsonStr, typeToken.getType());
33+
return map == null ? Collections.emptyMap() : map;
34+
}
35+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package example.http;
2+
3+
import javax.servlet.http.HttpServletRequest;
4+
import java.io.BufferedReader;
5+
import java.io.IOException;
6+
import java.util.Collections;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
import java.util.Optional;
10+
11+
/**
12+
* Graphql clients can send GET or POST HTTP requests. The spec does not make an explicit
13+
* distinction. So you may need to handle both. The following was tested using
14+
* a graphiql client tool found here : https://github.com/skevy/graphiql-app
15+
*
16+
* You should consider bundling graphiql in your application
17+
*
18+
* https://github.com/graphql/graphiql
19+
*
20+
* This outlines more information on how to handle parameters over http
21+
*
22+
* http://graphql.org/learn/serving-over-http/
23+
*/
24+
class QueryParameters {
25+
26+
String query;
27+
String operationName;
28+
Map<String, Object> variables = Collections.emptyMap();
29+
30+
public String getQuery() {
31+
return query;
32+
}
33+
34+
public String getOperationName() {
35+
return operationName;
36+
}
37+
38+
public Map<String, Object> getVariables() {
39+
return variables;
40+
}
41+
42+
static QueryParameters from(HttpServletRequest request) {
43+
QueryParameters parameters = new QueryParameters();
44+
if ("POST".equalsIgnoreCase(request.getMethod())) {
45+
//
46+
// the graphql guidance says :
47+
//
48+
// If the "application/graphql" Content-Type header is present, treat
49+
// the HTTP POST body contents as the GraphQL query string.
50+
if ("application/graphql".equals(request.getContentType())) {
51+
parameters.query = readPostBody(request);
52+
} else {
53+
Map<String, Object> json = readJSON(request);
54+
55+
//
56+
// the graphql guidance says :
57+
//
58+
// - If the "query" query string parameter is present
59+
// - ..., it should be parsed and handled
60+
// - in the same way as the HTTP GET case.
61+
//
62+
Optional<String> queryStr = getQueryQueryParameter(request);
63+
parameters.query = queryStr.orElseGet(() -> (String) json.get("query"));
64+
parameters.operationName = (String) json.get("operationName");
65+
parameters.variables = getVariablesFromPost(json.get("variables"));
66+
}
67+
} else {
68+
parameters.query = request.getParameter("query");
69+
parameters.operationName = request.getParameter("operationName");
70+
parameters.variables = getVariablesFromPost(request.getParameter("variables"));
71+
}
72+
return parameters;
73+
}
74+
75+
private static Optional<String> getQueryQueryParameter(HttpServletRequest request) {
76+
// http servlet spec getParameter() does not distinguish between POST body or GET query strings
77+
// so we have to parse for it in this case
78+
return HttpKit.getQueryParameter(request,"query");
79+
}
80+
81+
private static Map<String, Object> getVariablesFromPost(Object variables) {
82+
if (variables instanceof Map) {
83+
Map<?, ?> inputVars = (Map) variables;
84+
Map<String, Object> vars = new HashMap<>();
85+
inputVars.forEach((k, v) -> vars.put(String.valueOf(k), v));
86+
return vars;
87+
}
88+
return JsonKit.toMap(String.valueOf(variables));
89+
}
90+
91+
private static Map<String, Object> readJSON(HttpServletRequest request) {
92+
String s = readPostBody(request);
93+
return JsonKit.toMap(s);
94+
}
95+
96+
private static String readPostBody(HttpServletRequest request) {
97+
try {
98+
StringBuilder sb = new StringBuilder();
99+
BufferedReader reader = request.getReader();
100+
int c;
101+
while ((c = reader.read()) != -1) {
102+
sb.append((char) c);
103+
}
104+
return sb.toString();
105+
} catch (IOException e) {
106+
throw new RuntimeException(e);
107+
}
108+
}
109+
110+
}

src/test/groovy/graphql/StarWarsData.groovy

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import graphql.schema.DataFetcher
44
import graphql.schema.DataFetchingEnvironment
55
import graphql.schema.GraphQLObjectType
66
import graphql.schema.TypeResolver
7+
import graphql.schema.idl.EnumValuesProvider
78

89
class StarWarsData {
910

@@ -130,4 +131,14 @@ class StarWarsData {
130131
}
131132
}
132133

134+
static EnumValuesProvider episodeResolver = new EnumValuesProvider() {
135+
@Override
136+
Object getValue(String name) {
137+
switch (name) {
138+
case "NEWHOPE": return 4
139+
case "EMPIRE": return 5
140+
case "JEDI": return 6
141+
}
142+
}
143+
}
133144
}

0 commit comments

Comments
 (0)