Merge modules

This commit merges the common, webmvc and webflux modules into a single
web module for supporting GraphQL with all Spring web frameworks.
We're working here with optional dependencies to build the support
for Spring MVC on one side and Spring WebFlux on the other.

Also, this commit updates the starter modules to adopt the official
naming convention. For now, starter modules will host both the
configuration infrastructure and the transitive dependencies to activate
support.
This commit is contained in:
Brian Clozel
2020-09-15 13:28:55 +02:00
parent b39f12ed4e
commit a44c836fc0
31 changed files with 32 additions and 51 deletions

View File

@@ -0,0 +1,16 @@
description = "GraphQL Java support with Spring Web frameworks"
dependencies {
api "com.graphql-java:graphql-java"
compileOnly "org.springframework:spring-context"
compileOnly "org.springframework:spring-webflux"
compileOnly 'org.springframework:spring-webmvc'
compileOnly 'javax.servlet:javax.servlet-api'
testImplementation "com.fasterxml.jackson.core:jackson-databind"
testImplementation "org.springframework:spring-webflux"
testImplementation 'org.springframework:spring-webmvc'
testImplementation 'javax.servlet:javax.servlet-api'
}

View File

@@ -0,0 +1,5 @@
package org.springframework.graphql;
public class DefaultGraphQLInterceptor implements GraphQLInterceptor {
}

View File

@@ -0,0 +1,57 @@
package org.springframework.graphql;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
public class GraphQLHandler {
private GraphQL graphQL;
private GraphQLInterceptor interceptor;
public GraphQLHandler(GraphQL graphQL, GraphQLInterceptor interceptor) {
this.graphQL = graphQL;
this.interceptor = interceptor;
}
public Mono<GraphQLHttpResponse> graphqlPOST(GraphQLHttpRequest graphQLHttpRequest) {
String query = graphQLHttpRequest.getQuery();
ExecutionInput input = ExecutionInput.newExecutionInput()
.query(query)
.operationName(graphQLHttpRequest.getOperationName())
.variables(graphQLHttpRequest.getVariables())
.build();
MultiValueMap<String, String> requestParams = graphQLHttpRequest.getRequestParams();
Mono<ExecutionInput> executionInput = interceptor.preHandle(input,
graphQLHttpRequest.getHttpHeaders(),
requestParams);
return executionInput
.flatMap(this::execute)
.flatMap(result -> interceptor.postHandle(result, graphQLHttpRequest.getHttpHeaders(), requestParams))
.flatMap(result -> toResponseBody(result, graphQLHttpRequest));
}
private Mono<GraphQLHttpResponse> toResponseBody(ExecutionResult executionResult, GraphQLHttpRequest graphQLHttpRequest) {
Map<String, Object> responseBodyRaw = executionResult.toSpecification();
Object data = responseBodyRaw.get("data");
List<Map<String, Object>> errors = (List<Map<String, Object>>) responseBodyRaw.get("errors");
Map<String, Object> extensions = (Map<String, Object>) responseBodyRaw.get("extensions");
GraphQLHttpResponse responseBody = new GraphQLHttpResponse(data,
errors,
extensions,
new HttpHeaders());
Mono<GraphQLHttpResponse> graphQLResponseBodyMono = interceptor.customizeGraphQLHttpResponse(responseBody, executionResult, graphQLHttpRequest);
return graphQLResponseBodyMono;
}
protected Mono<ExecutionResult> execute(ExecutionInput input) {
return Mono.fromCompletionStage(graphQL.executeAsync(input));
}
}

View File

@@ -0,0 +1,46 @@
package org.springframework.graphql;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
import java.util.Map;
public class GraphQLHttpRequest {
private final String query;
private final String operationName;
private final Map<String, Object> variables;
private final HttpHeaders httpHeaders;
private final MultiValueMap<String, String> requestParams;
public GraphQLHttpRequest(String query,
String operationName,
Map<String, Object> variables,
HttpHeaders httpHeaders,
MultiValueMap<String, String> requestParams) {
this.query = query;
this.operationName = operationName;
this.variables = variables;
this.httpHeaders = httpHeaders;
this.requestParams = requestParams;
}
public String getQuery() {
return query;
}
public String getOperationName() {
return operationName;
}
public Map<String, Object> getVariables() {
return variables;
}
public HttpHeaders getHttpHeaders() {
return httpHeaders;
}
public MultiValueMap<String, String> getRequestParams() {
return requestParams;
}
}

View File

@@ -0,0 +1,40 @@
package org.springframework.graphql;
import org.springframework.http.HttpHeaders;
import java.util.List;
import java.util.Map;
public class GraphQLHttpResponse {
private final Object data;
private final List<Map<String, Object>> errors;
private final Map<String, Object> extensions;
private final HttpHeaders httpHeaders;
public GraphQLHttpResponse(Object data,
List<Map<String, Object>> errors,
Map<String, Object> extensions,
HttpHeaders httpHeaders) {
this.data = data;
this.errors = errors;
this.extensions = extensions;
this.httpHeaders = httpHeaders;
}
public Object getData() {
return data;
}
public List<Map<String, Object>> getErrors() {
return errors;
}
public Map<String, Object> getExtensions() {
return extensions;
}
public HttpHeaders getHttpHeaders() {
return httpHeaders;
}
}

View File

@@ -0,0 +1,30 @@
package org.springframework.graphql;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Mono;
public interface GraphQLInterceptor {
default Mono<ExecutionInput> preHandle(ExecutionInput input,
HttpHeaders headers,
MultiValueMap<String, String> requestParams) {
return Mono.just(input);
}
default Mono<ExecutionResult> postHandle(ExecutionResult result,
HttpHeaders httpHeaders,
MultiValueMap<String, String> requestParams) {
return Mono.just(result);
}
default Mono<GraphQLHttpResponse> customizeGraphQLHttpResponse(GraphQLHttpResponse graphQLHttpResponse,
ExecutionResult executionResult,
GraphQLHttpRequest graphQLHttpRequest) {
return Mono.just(graphQLHttpResponse);
}
}

View File

@@ -0,0 +1,6 @@
@NonNullApi
@NonNullFields
package org.springframework.graphql;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -0,0 +1,86 @@
package org.springframework.graphql.reactive.components;
import graphql.GraphQL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.DefaultGraphQLInterceptor;
import org.springframework.graphql.GraphQLHandler;
import org.springframework.graphql.GraphQLHttpRequest;
import org.springframework.graphql.GraphQLInterceptor;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class GraphQLController {
@Autowired
GraphQL graphQL;
GraphQLHandler graphQLHandler;
@Autowired(required = false)
GraphQLInterceptor graphQLInterceptor;
@PostConstruct
public void init() {
GraphQLInterceptor interceptor = graphQLInterceptor == null ? new DefaultGraphQLInterceptor() : graphQLInterceptor;
this.graphQLHandler = new GraphQLHandler(graphQL, interceptor);
}
@Bean
public RouterFunction<ServerResponse> routerFunction() {
RouterFunction<ServerResponse> route = route()
.POST("/graphql", this::graphqlPOST)
.build();
return route;
}
private Mono<ServerResponse> graphqlPOST(ServerRequest serverRequest) {
Mono<GraphQLReactiveRequestBody> bodyMono = serverRequest.bodyToMono(GraphQLReactiveRequestBody.class);
return bodyMono.flatMap(body -> {
String query = body.getQuery();
if (query == null) {
query = "";
}
Map<String, Object> variables = body.getVariables();
if (variables == null) {
variables = Collections.emptyMap();
}
GraphQLHttpRequest graphQLHttpRequest = new GraphQLHttpRequest(
query,
body.getOperationName(),
variables,
serverRequest.headers().asHttpHeaders(),
serverRequest.queryParams());
return graphQLHandler.graphqlPOST(graphQLHttpRequest);
}).flatMap(graphQLHttpResponse -> {
//TODO: this should be handled better:
// we don't want to serialize `null` values for `errors` and `extensions`
// this is why we convert it to a Map here
Map<String, Object> responseBodyRaw = new LinkedHashMap<>();
responseBodyRaw.put("data", graphQLHttpResponse.getData());
if (graphQLHttpResponse.getErrors() != null) {
responseBodyRaw.put("errors", graphQLHttpResponse.getErrors());
}
if (graphQLHttpResponse.getExtensions() != null) {
responseBodyRaw.put("extensions", graphQLHttpResponse.getExtensions());
}
return ServerResponse.ok().headers(httpHeaders -> {
httpHeaders.addAll(graphQLHttpResponse.getHttpHeaders());
}).bodyValue(responseBodyRaw);
});
}
}

View File

@@ -0,0 +1,33 @@
package org.springframework.graphql.reactive.components;
import java.util.Map;
public class GraphQLReactiveRequestBody {
private String query;
private String operationName;
private Map<String, Object> variables;
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public String getOperationName() {
return operationName;
}
public void setOperationName(String operationName) {
this.operationName = operationName;
}
public Map<String, Object> getVariables() {
return variables;
}
public void setVariables(Map<String, Object> variables) {
this.variables = variables;
}
}

View File

@@ -0,0 +1,6 @@
@NonNullApi
@NonNullFields
package org.springframework.graphql.reactive.components;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -0,0 +1,100 @@
package org.springframework.graphql.servlet.components;
import graphql.GraphQL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.DefaultGraphQLInterceptor;
import org.springframework.graphql.GraphQLHandler;
import org.springframework.graphql.GraphQLHttpRequest;
import org.springframework.graphql.GraphQLInterceptor;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.springframework.web.servlet.function.RouterFunctions.route;
@Configuration
public class GraphQLController {
@Autowired
GraphQL graphQL;
GraphQLHandler graphQLHandler;
@Autowired(required = false)
GraphQLInterceptor graphQLInterceptor;
@PostConstruct
public void init() {
GraphQLInterceptor interceptor = graphQLInterceptor == null ? new DefaultGraphQLInterceptor() : graphQLInterceptor;
this.graphQLHandler = new GraphQLHandler(graphQL, interceptor);
}
@Bean
public RouterFunction<ServerResponse> routerFunction() {
RouterFunction<ServerResponse> route = route()
.POST("/graphql", RequestPredicates.contentType(MediaType.APPLICATION_JSON)
.or(RequestPredicates.contentType(MediaType.APPLICATION_JSON_UTF8)), this::graphqlPOST)
.build();
return route;
}
private ServerResponse graphqlPOST(ServerRequest serverRequest) {
GraphQLServletRequestBody body = null;
try {
body = serverRequest.body(GraphQLServletRequestBody.class);
} catch (ServletException | IOException e) {
e.printStackTrace();
}
String query = body.getQuery();
if (query == null) {
query = "";
}
Map<String, Object> variables = body.getVariables();
if (variables == null) {
variables = Collections.emptyMap();
}
GraphQLHttpRequest graphQLHttpRequest = new GraphQLHttpRequest(
query,
body.getOperationName(),
variables,
serverRequest.headers().asHttpHeaders(),
serverRequest.params());
Mono<Map<String, Object>> responseRawMono = graphQLHandler.graphqlPOST(graphQLHttpRequest)
.map(graphQLHttpResponse -> {
//TODO: this should be handled better:
// we don't want to serialize `null` values for `errors` and `extensions`
// this is why we convert it to a Map here
Map<String, Object> responseBodyRaw = new LinkedHashMap<>();
responseBodyRaw.put("data", graphQLHttpResponse.getData());
if (graphQLHttpResponse.getErrors() != null) {
responseBodyRaw.put("errors", graphQLHttpResponse.getErrors());
}
if (graphQLHttpResponse.getExtensions() != null) {
responseBodyRaw.put("extensions", graphQLHttpResponse.getExtensions());
}
return responseBodyRaw;
});
//TODO: how to add http headers for the response here? It is part of responseRawMono but
// ServerResponse don't accept a CompletableFuture or Mono as headers.
return ServerResponse.ok().body(responseRawMono);
}
}

View File

@@ -0,0 +1,33 @@
package org.springframework.graphql.servlet.components;
import java.util.Map;
public class GraphQLServletRequestBody {
private String query;
private String operationName;
private Map<String, Object> variables;
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public String getOperationName() {
return operationName;
}
public void setOperationName(String operationName) {
this.operationName = operationName;
}
public Map<String, Object> getVariables() {
return variables;
}
public void setVariables(Map<String, Object> variables) {
this.variables = variables;
}
}

View File

@@ -0,0 +1,6 @@
@NonNullApi
@NonNullFields
package org.springframework.graphql.servlet.components;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;