GQLServlet.java
package net.morimekta.providence.graphql;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.morimekta.providence.PApplicationException;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageVariant;
import net.morimekta.providence.PProcessor;
import net.morimekta.providence.PServiceCall;
import net.morimekta.providence.PServiceCallType;
import net.morimekta.providence.PUnion;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.graphql.errors.GQLError;
import net.morimekta.providence.graphql.errors.GQLErrorLocation;
import net.morimekta.providence.graphql.errors.GQLErrorResponse;
import net.morimekta.providence.graphql.gql.GQLField;
import net.morimekta.providence.graphql.gql.GQLFragment;
import net.morimekta.providence.graphql.gql.GQLIntrospection;
import net.morimekta.providence.graphql.gql.GQLMethodCall;
import net.morimekta.providence.graphql.gql.GQLOperation;
import net.morimekta.providence.graphql.gql.GQLQuery;
import net.morimekta.providence.graphql.gql.GQLSelection;
import net.morimekta.providence.graphql.introspection.Type;
import net.morimekta.providence.graphql.introspection.TypeArguments;
import net.morimekta.providence.graphql.parser.GQLException;
import net.morimekta.providence.graphql.parser.GQLParser;
import net.morimekta.providence.graphql.utils.FieldFieldProvider;
import net.morimekta.providence.graphql.utils.InputValueFieldProvider;
import net.morimekta.providence.graphql.utils.TypeFieldProvider;
import net.morimekta.providence.serializer.JsonSerializer;
import net.morimekta.util.Binary;
import net.morimekta.util.Pair;
import net.morimekta.util.Tuple;
import net.morimekta.util.collect.UnmodifiableMap;
import net.morimekta.util.io.IOUtils;
import net.morimekta.util.io.IndentedPrintWriter;
import net.morimekta.util.json.JsonWriter;
import net.morimekta.util.json.PrettyJsonWriter;
import net.morimekta.util.lexer.LexerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static java.util.Objects.requireNonNull;
import static net.morimekta.providence.PApplicationExceptionType.PROTOCOL_ERROR;
import static net.morimekta.util.collect.UnmodifiableList.listOf;
/**
* A servlet for serving graphql given a GQL service and associated providers.
*
* Spec for serving GraphQL over HTTP can be found <a href="https://graphql.org/learn/serving-over-http/">here</a>.
*/
@SuppressWarnings("rawtypes")
public class GQLServlet extends HttpServlet {
private static final Logger LOGGER = LoggerFactory.getLogger(GQLServlet.class);
public static final String CONTENT_TYPE = "Content-Type";
public static final String MEDIA_TYPE = "application/graphql";
private static final String PRETTY_PARAM = "pretty";
private static final String TRUE = "true";
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final JavaType VARIABLES;
static {
VARIABLES = MAPPER.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class);
}
private final GQLContextFactory contextFactory;
private final GQLDefinition definition;
private final GQLProcessorProvider queryProvider;
private final GQLProcessorProvider mutationProvider;
private final GQLParser parser;
private final ExecutorService executorService;
private final Map<PField<?>, GQLFieldProvider<?>> fieldProviderMap;
@SuppressWarnings("unused")
@JsonIgnoreProperties(ignoreUnknown = true)
private static class JsonRequest {
@JsonProperty("query")
String query;
@JsonProperty("operationName")
String operationName;
@JsonProperty("variables")
Map<String, Object> variables;
}
public static class Builder {
private GQLDefinition definition;
private GQLContextFactory<?> contextFactory;
private GQLProcessorProvider<?> queryProvider;
private GQLProcessorProvider<?> mutationProvider;
private Collection<GQLFieldProvider<?>> fieldProviders;
private ExecutorService executorService;
private Builder(@Nonnull GQLDefinition definition) {
this.fieldProviders = new ArrayList<>();
this.definition = definition;
}
public Builder context(GQLContextFactory<?> contextFactory) {
this.contextFactory = contextFactory;
return this;
}
public Builder query(@Nonnull GQLProcessorProvider<?> queryProvider) {
this.queryProvider = queryProvider;
return this;
}
public Builder mutation(@Nonnull GQLProcessorProvider<?> mutationProvider) {
this.mutationProvider = mutationProvider;
return this;
}
public Builder fieldProvider(GQLFieldProvider<?>... fieldProviders) {
this.fieldProviders.addAll(Arrays.asList(fieldProviders));
return this;
}
public Builder executor(@Nonnull ExecutorService executorService) {
this.executorService = executorService;
return this;
}
@SuppressWarnings("unchecked")
public GQLServlet build() {
if (queryProvider == null) throw new IllegalStateException("No query provider");
return new GQLServlet(definition,
queryProvider,
mutationProvider,
fieldProviders,
Optional.ofNullable(contextFactory)
.orElse(GQLContextFactory.DEFAULT_INSTANCE),
Optional.ofNullable(executorService)
.orElseGet(() -> Executors.newFixedThreadPool(10)));
}
}
public static Builder builder(GQLDefinition definition) {
return new Builder(definition);
}
@SuppressWarnings("unchecked")
public GQLServlet(@Nonnull GQLDefinition definition,
@Nonnull GQLProcessorProvider<?> queryProvider,
@Nullable GQLProcessorProvider<?> mutationProvider,
@Nonnull Collection<GQLFieldProvider<?>> fieldProviders,
@Nullable GQLContextFactory<?> contextFactory,
@Nonnull ExecutorService executorService) {
if ((definition.getMutation() == null) != (mutationProvider == null)) {
throw new IllegalArgumentException("Definition " +
(definition.getMutation() == null ? "does not have" : "has") +
" mutation, while provider is " +
(mutationProvider == null ? "not " : "") +
"present");
}
Map<PField<?>, GQLFieldProvider<?>> providerMap = new HashMap<>();
buildMutatorMap(providerMap, new FieldFieldProvider(definition));
buildMutatorMap(providerMap, new InputValueFieldProvider(definition));
buildMutatorMap(providerMap, new TypeFieldProvider(definition));
for (GQLFieldProvider<?> provider : fieldProviders) {
buildMutatorMap(providerMap, provider);
}
this.contextFactory = Optional
.ofNullable(contextFactory)
.orElse(GQLContextFactory.DEFAULT_INSTANCE);
this.definition = definition;
this.queryProvider = queryProvider;
this.mutationProvider = mutationProvider;
this.fieldProviderMap = UnmodifiableMap.copyOf(providerMap);
this.parser = new GQLParser(definition);
this.executorService = executorService;
}
/**
* Only non-mutation GQL queries may be handed to a get request, putting the
* actual query in the query= parameter.
*
* @param req The HTTP request.
* @param resp The HTTP response.
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
GQLQuery query;
String operationName;
try {
Map<String, Object> rawVariables = Collections.emptyMap();
String variables = req.getParameter("variables");
if (variables != null) {
rawVariables = MAPPER.readValue(variables, VARIABLES);
}
operationName = req.getParameter("operationName");
String q = req.getParameter("query");
if (q == null) {
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true),
new GQLError("No query param in request", null));
return;
}
query = parser.parseQuery(q, rawVariables);
} catch (LexerException e) {
LOGGER.debug("Error parsing query:\n{}", e.displayString(), e);
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true),
new GQLError(e.getMessage(),
listOf(new GQLErrorLocation(e.getLineNo(), e.getLinePos()))));
return;
} catch (IOException | IllegalArgumentException e) {
LOGGER.debug("Exception in parsing query: {}", e.getMessage(), e);
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true), new GQLError(e.getMessage(), null));
return;
}
if (operationName == null && !query.isDefaultOperationAvailable()) {
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true), new GQLError(
"No default query defined", null));
return;
}
GQLOperation operation;
try {
operation = query.getOperation(operationName).orElse(null);
} catch (IllegalArgumentException e) {
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true), new GQLError(
e.getMessage(), null));
return;
}
if (operation == null) {
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true), new GQLError(
"No such operation " + operationName + " in query", null));
return;
}
if (operation.isMutation()) {
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true), new GQLError(
"mutation not allowed in GET request", null));
return;
}
handleOperation(req, resp, operation, false);
}
/**
* Both normal GQL queries and mutations may be posted. The
*
* @param req The HTTP request.
* @param resp The HTTP response.
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String contentType = req.getContentType();
if (contentType == null) {
contentType = MEDIA_TYPE;
} else {
contentType = contentType.split(";")[0].trim().toLowerCase(Locale.US);
}
GQLOperation operation;
try {
GQLQuery query = null;
switch (contentType) {
case MEDIA_TYPE: {
String q = IOUtils.readString(req.getReader());
query = parser.parseQuery(q, new HashMap<>());
operation = query.getOperation(null)
.orElseThrow(() -> new IllegalArgumentException("No default operation defined in query"));
break;
}
case JsonSerializer.JSON_MEDIA_TYPE: {
JsonRequest content;
try {
content = MAPPER.readValue(req.getReader(), JsonRequest.class);
} catch (JsonParseException e) {
throw e;
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
if (content.query != null) {
query = parser.parseQuery(content.query, content.variables);
}
if (query == null) {
String q = req.getParameter("query");
if (q == null) {
throw new IllegalArgumentException("No query in request");
}
query = parser.parseQuery(q, Optional.ofNullable(content.variables)
.orElse(new HashMap<>()));
}
operation = query.getOperation(null).orElseThrow(() -> new IllegalArgumentException("No default operation defined in query"));
break;
}
default: {
throw new IllegalArgumentException("Unknown content-type: " + contentType);
}
}
} catch (JsonParseException e) {
LOGGER.debug("Error parsing JSON: {}", e.getMessage(), e);
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true), new GQLError(e.getMessage(), listOf(
new GQLErrorLocation(e.getLocation().getLineNr(), e.getLocation().getColumnNr()))));
return;
} catch (LexerException e) {
LOGGER.debug("Error parsing query:\n{}", e.displayString(), e);
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true),
new GQLError(e.getMessage(),
listOf(new GQLErrorLocation(e.getLineNo(), e.getLinePos()))));
return;
} catch (IOException | IllegalArgumentException e) {
LOGGER.debug("Exception in parsing query: {}", e.getMessage(), e);
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
writeError(makeWriter(resp, true), new GQLError(e.getMessage(), null));
return;
}
handleOperation(req, resp, operation, operation.isMutation());
}
/**
* Handle operation.
*
* @param req The HTTP request.
* @param resp The HTTP response.
* @param operation The operation to be done.
* @param handleSerially If the operation should be handled serially.
* If true will create a single context and handle all method
* calls after one another and stop on first error.
* If false will spawn one thread per method call, and write
* a data response or error separately for each call.
* @throws IOException If operation handle failed.
*/
@OverridingMethodsMustInvokeSuper
protected void handleOperation(@Nonnull HttpServletRequest req,
@Nonnull HttpServletResponse resp,
@Nonnull GQLOperation operation,
boolean handleSerially) throws IOException {
try {
if (handleSerially) {
handleSerially(req, resp, operation);
} else {
handleParallel(req, resp, operation);
}
} catch (IOException e) {
LOGGER.error("Error handling {}: {}",
operation.isMutation() ? "mutation" : "query",
e.getMessage(),
e);
throw e;
}
}
private JsonWriter makeWriter(HttpServletResponse resp, boolean pretty) throws IOException {
return pretty ?
new PrettyJsonWriter(new IndentedPrintWriter(resp.getWriter(), " ", "\n")) :
new JsonWriter(resp.getWriter());
}
@SuppressWarnings("unchecked,rawtypes")
private void handleSerially(HttpServletRequest req,
HttpServletResponse resp,
GQLOperation operation) throws IOException {
boolean pretty = TRUE.equalsIgnoreCase(req.getParameter(PRETTY_PARAM));
resp.setStatus(HttpServletResponse.SC_OK);
resp.setHeader("Content-Type", JsonSerializer.JSON_MEDIA_TYPE);
JsonWriter writer = makeWriter(resp, pretty);
writer.object()
.key("data")
.object();
// run serially
int i = 0;
PServiceCall<?> request = null;
try (GQLContext context = contextFactory.createContext(req, operation)) {
try {
for (GQLSelection entry : operation.getSelectionSet()) {
if (entry instanceof GQLMethodCall) {
GQLMethodCall methodEntry = (GQLMethodCall) entry;
request = new PServiceCall<>(requireNonNull(methodEntry.getMethod()).getName(),
PServiceCallType.CALL,
++i,
(PMessage) methodEntry.getArguments());
PProcessor processor = mutationProvider.processorFor(context, methodEntry);
PServiceCall<?> response = processor.handleCall(request);
if (response.getType() == PServiceCallType.EXCEPTION) {
throw (PApplicationException) response.getMessage();
} else {
PUnion<?> union = (PUnion) response.getMessage();
if (union.unionField().getId() == 0) {
Object out = union.get(0);
if (methodEntry.getAlias() != null) {
writer.key(methodEntry.getAlias());
} else {
writer.key(methodEntry.getMethod().getName());
}
writeAndHandleResponse(writer, out, methodEntry);
} else {
throw (Exception) union.get(union.unionField().getId());
}
}
} else if (entry instanceof GQLIntrospection) {
GQLIntrospection intro = (GQLIntrospection) entry;
switch (intro.getField()) {
case __schema: {
if (intro.getAlias() != null) {
writer.key(intro.getAlias());
} else {
writer.key(intro.getField().name());
}
writeAndHandleResponse(writer, definition.getIntrospectionSchema(),
new GQLField(Type._Field.NAME, intro.getSelectionSet()));
break;
}
case __type: {
TypeArguments ta = (TypeArguments) intro.getArguments();
if (ta == null || ta.getName() == null) {
throw new PApplicationException("No name for type lookup", PROTOCOL_ERROR);
} else {
Type type = definition.getIntrospectionType(ta.getName());
if (type == null) {
throw new PApplicationException("Unknown type named '" + ta.getName() + "'",
PROTOCOL_ERROR);
} else {
if (intro.getAlias() != null) {
writer.key(intro.getAlias());
} else {
writer.key(intro.getField().name());
}
writeAndHandleResponse(writer, type,
new GQLField(Type._Field.NAME, intro.getSelectionSet()));
}
}
break;
}
case __typename: {
throw new PApplicationException("Introspection __typename not allowed at root",
PROTOCOL_ERROR);
}
}
} else {
throw new GQLException("Unhandled root selection: " + entry.toString());
}
}
context.commit();
writer.endObject()
.endObject();
} catch (Exception e) {
LOGGER.debug("Call exception: {}", e.getMessage(), e);
context.abort(e);
writer.endObject();
writeErrors(writer, listOf(Pair.create(request, e)));
writer.endObject();
}
} catch (Exception e) {
LOGGER.debug("Unhandled exception in abort: {}", e.getMessage(), e);
writer.endObject();
writeErrors(writer, listOf(Pair.create(request, e)));
writer.endObject();
} finally {
writer.flush();
}
}
private void handleParallel(HttpServletRequest req,
HttpServletResponse resp,
GQLOperation operation) throws IOException {
boolean pretty = TRUE.equalsIgnoreCase(req.getParameter(PRETTY_PARAM));
List<Tuple.Tuple3<GQLSelection, PServiceCall, Future<PServiceCall>>> futures = new ArrayList<>();
// run in parallel
int i = 0;
for (GQLSelection entry : operation.getSelectionSet()) {
if (entry instanceof GQLIntrospection) {
GQLIntrospection intro = (GQLIntrospection) entry;
futures.add(Tuple.tuple(intro, null, null));
} else if (entry instanceof GQLMethodCall) {
GQLMethodCall methodEntry = (GQLMethodCall) entry;
@SuppressWarnings("unchecked")
PMessage arguments = methodEntry.getArguments();
if (arguments == null) {
arguments = methodEntry.getMethod().getRequestType().builder().build();
}
@SuppressWarnings("unchecked")
PServiceCall<?> psc = new PServiceCall<>(
methodEntry.getMethod().getName(),
PServiceCallType.CALL,
++i,
arguments);
futures.add(Tuple.tuple(methodEntry, psc, executorService.submit(() -> {
try (GQLContext context = contextFactory.createContext(req, operation)) {
try {
@SuppressWarnings("unchecked")
PProcessor processor = queryProvider.processorFor(context, methodEntry);
PServiceCall response = processor.handleCall(psc);
context.commit();
return response;
} catch (Exception e) {
LOGGER.trace("Exception handling {}", methodEntry.getMethod().getName(), e);
context.abort(e);
throw e;
}
}
})));
} else {
throw new GQLException("Unhandled root selection: " + entry.toString());
}
}
resp.setStatus(HttpServletResponse.SC_OK);
resp.setHeader(CONTENT_TYPE, JsonSerializer.JSON_MEDIA_TYPE);
JsonWriter writer = makeWriter(resp, pretty);
try {
writer.object()
.key("data")
.object();
List<Pair<PServiceCall, Exception>> errors = new ArrayList<>();
for (Tuple.Tuple3<GQLSelection, PServiceCall, Future<PServiceCall>> future : futures) {
if (future.first() instanceof GQLIntrospection) {
GQLIntrospection intro = (GQLIntrospection) future.first();
switch (intro.getField()) {
case __schema: {
if (intro.getAlias() != null) {
writer.key(intro.getAlias());
} else {
writer.key(intro.getField().name());
}
writeAndHandleResponse(writer, definition.getIntrospectionSchema(),
new GQLField(Type._Field.NAME, intro.getSelectionSet()));
break;
}
case __type: {
TypeArguments ta = (TypeArguments) intro.getArguments();
if (ta == null || ta.getName() == null) {
errors.add(Pair.create(new PServiceCall<>("__type",
PServiceCallType.CALL,
0,
Type.builder().build()),
new IllegalArgumentException("No name for type lookup")));
} else {
Type type = definition.getIntrospectionType(ta.getName());
if (type == null) {
errors.add(Pair.create(new PServiceCall<>("__type",
PServiceCallType.CALL,
0,
Type.builder().build()),
new IllegalArgumentException(
"Unknown type named '" + ta.getName() + "'")));
} else {
if (intro.getAlias() != null) {
writer.key(intro.getAlias());
} else {
writer.key(intro.getField().name());
}
writeAndHandleResponse(writer, type,
new GQLField(Type._Field.NAME, intro.getSelectionSet()));
}
}
break;
}
case __typename: {
errors.add(Pair.create(new PServiceCall<>("__typename",
PServiceCallType.CALL,
0,
Type.builder().build()),
new IllegalArgumentException(
"Introspection __typename not allowed at root")));
}
}
} else {
GQLMethodCall field = (GQLMethodCall) future.first();
PServiceCall call = future.second();
try {
PServiceCall reply = future.third().get(3L, TimeUnit.MINUTES);
if (reply.getType() == PServiceCallType.EXCEPTION) {
errors.add(Pair.create(call, (PApplicationException) reply.getMessage()));
} else {
PUnion<?> response = (PUnion) reply.getMessage();
if (response.unionField().getId() == 0) {
Object out = response.get(0);
if (field.getAlias() != null) {
writer.key(field.getAlias());
} else {
writer.key(field.getMethod().getName());
}
writeAndHandleResponse(writer, out, field);
} else {
errors.add(Pair.create(call, response.get(response.unionField().getId())));
}
}
} catch (InterruptedException | ExecutionException |
TimeoutException | RuntimeException e) {
LOGGER.warn("Exception calling {}: {}", call.getMethod(), e.getMessage(), e);
errors.add(Pair.create(call, e));
}
}
}
writer.endObject();
writeErrors(writer, errors);
writer.endObject();
} finally {
writer.flush();
}
}
private void writeErrors(JsonWriter writer, List<Pair<PServiceCall, Exception>> errors) throws IOException {
if (errors.size() > 0) {
writer.key("errors")
.array();
for (Pair<PServiceCall, Exception> error : errors) {
if (error.second instanceof GQLError) {
writeAndHandleResponse(writer, error.second, new GQLField(GQLErrorResponse._Field.ERRORS));
} else if (error.first != null){
writer.object()
.key("message")
.value("Exception in call to " + error.first.getMethod() +
": " + error.second.getMessage())
.endObject();
} else {
writer.object()
.key("message")
.value("Exception in handling: " + error.second.getMessage())
.endObject();
}
}
writer.endArray();
}
}
private void writeError(JsonWriter writer, GQLError error) throws IOException {
writeAndHandleResponse(writer,
new GQLErrorResponse(listOf(error)),
new GQLField(GQLErrorResponse._Field.ERRORS));
}
private void writeAndHandleMessageResponse(JsonWriter writer, PMessage<?> message, GQLSelection msgField) throws IOException {
PMessageDescriptor descriptor = message.descriptor();
if (descriptor.getVariant() == PMessageVariant.UNION &&
descriptor.getImplementing() != null) {
PUnion union = (PUnion) message;
writeAndHandleResponse(writer, union.get(union.unionField().getId()), msgField);
return;
}
writer.object();
if (msgField.getSelectionSet() == null) {
// NOTE: This is not really supported by graphql, but can significantly
// simplify making test requests and looking at the possible fields to use
// before landing on a complete query.
for (PField field : descriptor.getFields()) {
if (message.has(field.getId())) {
if (field.getName().startsWith("__")) {
continue;
}
writeAndHandleField(writer, message, new GQLField(field));
}
}
} else {
writeAndHandleSelection(writer, message, msgField.getSelectionSet());
}
writer.endObject();
}
private void writeAndHandleSelection(
@Nonnull JsonWriter writer,
@Nonnull PMessage<?> message,
@Nonnull List<GQLSelection> selection) throws IOException {
PMessageDescriptor<?> descriptor = message.descriptor();
for (GQLSelection entry : selection) {
if (entry instanceof GQLIntrospection) {
GQLIntrospection intro = (GQLIntrospection) entry;
switch (intro.getField()) {
case __type: {
Type type = this.definition.getIntrospectionType(descriptor, false);
writer.key(GQLIntrospection.Field.__type.name());
writeAndHandleMessageResponse(writer, type, intro);
break;
}
case __typename: {
Type type = this.definition.getIntrospectionType(descriptor, false);
writer.key(GQLIntrospection.Field.__typename.name());
writer.value(type.getName());
break;
}
case __schema: {
// ignore.
break;
}
}
} else if (entry instanceof GQLFragment) {
GQLFragment fragment = (GQLFragment) entry;
if (fragment.isApplicableFor(descriptor)) {
writeAndHandleSelection(writer, message, fragment.getSelectionSet());
}
} else if (entry instanceof GQLField) {
writeAndHandleField(writer, message, (GQLField) entry);
} else {
throw new IOException("Unknown GQL entry " + entry.getClass().getSimpleName() + ": " + entry.toString());
}
}
}
@SuppressWarnings("unchecked")
private void writeAndHandleField(JsonWriter writer, PMessage<?> message, GQLField gqlField) throws IOException {
Object value;
// Up the field to make sure we have the message type
// field for the name. This is needed where the field
// requested is from an interface.
PField<?> actualField = requireNonNull(gqlField.getField());
actualField = message.descriptor().fieldForName(actualField.getName());
@SuppressWarnings("rawtypes")
GQLFieldProvider mutator = fieldProviderMap.get(actualField);
if (mutator != null) {
value = mutator.provide(message, actualField, gqlField);
} else if (message.has(actualField.getId())){
value = message.get(actualField.getId());
} else {
value = null;
}
if (gqlField.getAlias() != null) {
writer.key(gqlField.getAlias());
} else {
writer.key(actualField.getName());
}
writeAndHandleResponse(writer, value, gqlField);
}
@SuppressWarnings("rawtypes")
private void writeAndHandleResponse(JsonWriter writer, Object out, GQLSelection field) throws IOException {
if (out == null) {
writer.value((String) null);
return;
}
if (out instanceof PMessage) {
writeAndHandleMessageResponse(writer, (PMessage<?>) out, field);
} else {
if (out instanceof Integer ||
out instanceof Long ||
out instanceof Short ||
out instanceof Byte) {
writer.value(((Number) out).longValue());
} else if (out instanceof Double ||
out instanceof Float) {
writer.value(((Number) out).doubleValue());
} else if (out instanceof CharSequence) {
writer.value((CharSequence) out);
} else if (out instanceof Binary) {
writer.value((Binary) out);
} else if (out instanceof Boolean) {
writer.value((Boolean) out);
} else if (out instanceof Collection) {
writer.array();
for (Object o : (Collection) out) {
writeAndHandleResponse(writer, o, field);
}
writer.endArray();
} else {
writer.value(out.toString());
}
}
}
private static void buildMutatorMap(Map<PField<?>, GQLFieldProvider<?>> providerMap,
GQLFieldProvider<?> provider) {
for (PField<?> field : provider.getFields()) {
if (providerMap.containsKey(field)) {
throw new IllegalArgumentException(
"Two field providers claim to handle " +
provider.getDescriptor().getQualifiedName() + "." + field.getName() +
" at the same time.");
}
providerMap.put(field, provider);
}
}
}