All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.graphql_java_generator.client.request.AbstractGraphQLRequest Maven / Gradle / Ivy

There is a newer version: 1.18
Show newest version
/**
 * 
 */
package com.graphql_java_generator.client.request;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.eclipse.jetty.websocket.client.WebSocketClient;

import com.graphql_java_generator.annotation.GraphQLScalar;
import com.graphql_java_generator.annotation.RequestType;
import com.graphql_java_generator.client.GraphQLConfiguration;
import com.graphql_java_generator.client.GraphQLRequestObject;
import com.graphql_java_generator.client.SubscriptionCallback;
import com.graphql_java_generator.client.SubscriptionClient;
import com.graphql_java_generator.client.request.InputParameter.InputParameterType;
import com.graphql_java_generator.exception.GraphQLRequestExecutionException;
import com.graphql_java_generator.exception.GraphQLRequestPreparationException;

/**
 * This class contains the description for a GraphQL request that will be sent to the server. It's an abstract class,
 * and can not be use directly: a concrete class is generated by the plugin, when in client mode. This concrete class
 * provides all the necessary context to this abstract class for it to work properly.
* This class stores: *
    *
  • The query part, if any
  • *
  • The mutation part, if any
  • *
  • The subscription part, if any
  • *
  • The fragments, if any
  • *
* * @author etienne-sf */ public abstract class AbstractGraphQLRequest { /** * This contains the default configuration, that will apply if no local configuration has been defined for this * instance */ static GraphQLConfiguration staticConfiguration = null; /** * This contains the configuration for this instance. This configuration overrides the {@link #staticConfiguration}, * if defined. */ GraphQLConfiguration instanceConfiguration = null; /** The query, if any */ QueryField query = null; /** The mutation, if any */ QueryField mutation = null; /** The mutation, if any */ QueryField subscription = null; /** All the fragments defined for this query */ List fragments = new ArrayList<>(); /** The string that has been used to create this GraphQL request */ final String graphQLRequest; /** * Null if the request is a full request. Mandatory if the request is a partial request. When this GraphQLRequest is * built for a partial query, that is for a particular query/mutation/subscription, then fieldName states whether * this queryName is actually a query, a mutation or a subscription. */ RequestType requestType; /** * Null if the request is a full request. Mandatory if the request is a partial request.
* When this GraphQLRequest is built for a partial query, that is for a particular query/mutation/subscription, then * queryName is the name of the query, mutation or subscription. This allow to check that the GraphQLRequest is the * good to be executed for this partial query. */ final String requestName; /** * The package name, where the GraphQL generated classes are. It's used to load the class definition, and get the * GraphQL metadata coming from the GraphQL schema. */ protected final String packageName; /** Indicates what is being read, when reading the GraphQL variable list */ private enum Step { NAME, TYPE }; /** * Create the instance, from the GraphQL request, for a partial request.
* * Important note: this constructor SHOULD NOT be called by external application. Its signature * may change in the future. To prepare Partial Requests, application code SHOULD call the * getXxxxGraphQLRequests methods, that are generated in the query/mutation/subscription java classes. * * @param graphQLRequest * The partial GraphQL request, in text format. Writing partial request allows use to execute a * query/mutation/subscription, and only define what's expected as a response for this * query/mutation/subscription. You can send the parameters for this query/mutation/subscription as * parameter of the java method, without dealing with bind variable in the GraphQL query. Please read the * client doc * page for more information, including hints and limitations. * @param requestType * The information whether this queryName is actually a query, a mutation or a subscription * @param fieldName * The name of the query, mutation or subscription, for instance "createHuman", in the GraphQL request * "mutation {createHuman (...) { ...}}". * @param inputParams * The list of input parameters for this query/mutation/subscription * @throws GraphQLRequestPreparationException */ public AbstractGraphQLRequest(String graphQLRequest, RequestType requestType, String fieldName, InputParameter... inputParams) throws GraphQLRequestPreparationException { if (requestType == null) { throw new NullPointerException("requestType is mandatory, but a null value has been provided"); } if (fieldName == null) { throw new NullPointerException("fieldName is mandatory, but a null value has been provided"); } this.requestType = requestType; this.requestName = null; this.graphQLRequest = graphQLRequest; this.packageName = getGraphQLClassesPackageName(); QueryField field; switch (requestType) { case query: query = getQueryContext();// Get the query field from the concrete class field = new QueryField(query.clazz, fieldName); query.fields.add(field); break; case mutation: mutation = getMutationContext();// Get the mutation field from the concrete class field = new QueryField(mutation.clazz, fieldName); mutation.fields.add(field); break; case subscription: subscription = getSubscriptionContext();// Get the subscription field from the concrete class field = new QueryField(subscription.clazz, fieldName); subscription.fields.add(field); break; default: throw new GraphQLRequestPreparationException("Non managed request type '" + requestType + " while reading the GraphQL request: " + graphQLRequest); } // Let's add the input parameters to this new field field.inputParameters = Arrays.asList(inputParams); // Ok, we have to parse a string which looks like that: "query {human(id: &humanId) { id name friends{name}}}" // We tokenize the string, by using the space as a delimiter, and all other special GraphQL characters QueryTokenizer qt = new QueryTokenizer(this.graphQLRequest); // The graphQLRequest may be null (for instance for a scalar, or if we want the plugin to automatically add all // scalar fields for this query/mutation/subscription) if (!qt.hasMoreTokens()) { // Ok, we're done } else { // The first token must be a { // And we must read it first, before parsing the request content String token = qt.nextToken(); if (!"{".equals(token)) { throw new GraphQLRequestPreparationException( "The Partial GraphQL Request should start by a '{', but it doesn't: " + graphQLRequest); } field.readTokenizerForResponseDefinition(qt); } // Let's finish the job finishRequestPreparation(); } /** * Creates the GraphQL request, for a full request. It will: *
    *
  • Read the query and/or the mutation
  • *
  • Read all fragment definitions
  • *
  • For all non scalar field, subfields (and so on recursively), if they are empty (that is the query doesn't * define the requested fields of a non scalar field, then all its scalar fields are added)
  • *
  • Add the introspection __typename field to all scalar field list, if it doesnt't already exist. This is * necessary to allow proper deserialization of interfaces and unions.
  • *
* * @param graphQLRequest * The GraphQL request, in text format, as defined in the GraphQL specifications, and as it can be used * in GraphiQL. Please read the * client doc * page for more information, including hints and limitations. * * @throws GraphQLRequestPreparationException */ public AbstractGraphQLRequest(String graphQLRequest) throws GraphQLRequestPreparationException { String localQueryName = null; this.graphQLRequest = graphQLRequest; this.packageName = getGraphQLClassesPackageName(); this.requestType = RequestType.query; // query is the default value, as if there is no query, mutation or // subscription keyword, then it must be a query. boolean requestTypeHasBeenRead = false; // Used for a basic check in unknown tokens, to see if this token can be // the request name List inputParameters = new ArrayList<>(); // The list of GraphQL variables for this query // Ok, we have to parse a string which looks like that: "query {human(id: &humanId) { id name friends{name}}}" // We tokenize the string, by using the space as a delimiter, and all other special GraphQL characters QueryTokenizer qt = new QueryTokenizer(this.graphQLRequest); // We scan the input string. It may contain fragment definition and query/mutation/subscription while (qt.hasMoreTokens()) { String token = qt.nextToken(); switch (token) { case "fragment": fragments.add(new Fragment(qt, packageName, false, null)); break; case "query": case "mutation": case "subscription": requestType = RequestType.valueOf(token); requestTypeHasBeenRead = true;// We'll know accept an unknown token as the request name break; case "(": try { readRequestParameters(qt, inputParameters); } catch (Exception e) { throw new GraphQLRequestPreparationException( e.getMessage() + " (while reading the request parameters)", e); } break; case "{": // We read the query/mutation/subscription like any field. switch (requestType) { case query: query = getQueryContext();// Get the query field from the concrete class query.inputParameters = inputParameters; query.readTokenizerForResponseDefinition(qt); break; case mutation: mutation = getMutationContext();// Get the mutation field from the concrete class mutation.inputParameters = inputParameters; mutation.readTokenizerForResponseDefinition(qt); break; case subscription: subscription = getSubscriptionContext();// Get the subscription field from the concrete class subscription.inputParameters = inputParameters; subscription.readTokenizerForResponseDefinition(qt); break; default: throw new GraphQLRequestPreparationException("Non managed request type '" + requestType + " while reading the GraphQL request: " + graphQLRequest); } break; default: if (requestTypeHasBeenRead) { localQueryName = token; } else { throw new GraphQLRequestPreparationException( "Unknown token '" + token + " while reading the GraphQL request: " + graphQLRequest); } } } if (query == null && mutation == null && subscription == null) { throw new GraphQLRequestPreparationException("No response definition found"); } // Let's finish the job // As the query name can't be changed, we have to set in a temporary variable, to allow changing its value when // we found one this.requestName = localQueryName; finishRequestPreparation(); } /** * Reads the parameters of the request. These parameters are actually GraphQL variables, according to the GraphQL * spec. * * @param qt * The {@link QueryTokenizer} current token is the '(' that starts the parameter list. When the method * returns, the {@link QueryTokenizer} current token is the ')' * @param inputParameters * The empty list if {@link InputParameter}s. * @throws GraphQLRequestPreparationException */ private void readRequestParameters(QueryTokenizer qt, List inputParameters) throws GraphQLRequestPreparationException { String token; // We're reading the request parameters. It should be something like "($param1: Type1, $param2: Type2!)" Step step = Step.NAME; String name = null; while (true) { token = qt.nextToken(); // Are we done? if (token.equals(")")) { if (step.equals(Step.TYPE)) { throw new GraphQLRequestPreparationException( "Found a ')', while expecting a value for the '" + name + "' query parameter"); } else { // Ok we're done break; } } else if (token.equals(",")) { // We should be waiting for the name of the GraphQL variable if (!step.equals(Step.NAME)) { throw new GraphQLRequestPreparationException("unexpected ','"); } // Let's go to the next token, that should be the GraphQL type token = qt.nextToken(); } else if (token.equals(":")) { // We should be waiting for the type of the GraphQL variable if (!step.equals(Step.TYPE)) { throw new GraphQLRequestPreparationException("unexpected ':'"); } // Let's go to the next token, that should be the GraphQL type token = qt.nextToken(); } switch (step) { case NAME: if (!token.startsWith("$")) { throw new GraphQLRequestPreparationException( "The GraphQL variable names should start by a '$', but this one doesn't: '" + token + "'"); } // We store the name, without the leading '$' name = token.substring(1); // The next token should be the value step = Step.TYPE; break; case TYPE: // The current token is the GraphQL variable type, for instance "[[Human!]]!". Let's parse it. int currentDepth = 0; String graphQLTypeName = null; boolean mandatory = false; int listDepth = 0; boolean itemMandatory = false; while (true) { switch (token) { case "[": listDepth += 1; currentDepth += 1; break; case "]": currentDepth -= 1; break; case "!": // If we're here, it means the depth is at least one. itemMandatory = true; break; case ",": case ")":// Too bad, the query is wrongly written throw new GraphQLRequestPreparationException( "Syntax error in the query, while reading the type of the '" + name + "' parameter of the request"); default: // We have the GraphQL type name graphQLTypeName = token; } // Are we done? if (currentDepth == 0) { break; } token = qt.nextToken(); } ; // We get here if the item is not a list, or after reading the last ']'. // Let's check if there is a trailing '!' if (qt.checkNextToken("!")) { mandatory = true; // Then we pass this token token = qt.nextToken(); } else { mandatory = false; } inputParameters.add(InputParameter.newGraphQLVariableParameter(name, graphQLTypeName, mandatory, listDepth, itemMandatory)); // The next token should be either the end of parameters (with a ')') or a name step = Step.NAME; break; }// switch } // while } /** * This method executes the current GraphQL as a query or mutation GraphQL request, and return its * response mapped in the relevant POJO. This method executes a partial GraphQL query, or a full GraphQL * request.
* Note: Don't forget to free the server's resources by calling the {@link WebSocketClient#stop()} method of * the returned object. * * @param * @param t * The type of the POJO which should be returned. It must be the query or the mutation class, generated * by the plugin * @param params * @return * @throws GraphQLRequestExecutionException */ public T exec(Class t, Map params) throws GraphQLRequestExecutionException { if (instanceConfiguration != null) { return instanceConfiguration.getQueryExecutor().execute(this, params, t); } else if (staticConfiguration != null) { return staticConfiguration.getQueryExecutor().execute(this, params, t); } else { throw new GraphQLRequestExecutionException( "The GraphQLRequestConfiguration has not been set in the GraphQLRequest. " + "Please set either the GraphQL instance configuration " + "or the GraphQL static configuration before executing a GraphQL request"); } } /** * Execution of the given subscription GraphQL request, and return its response mapped in the relevant POJO. * This method executes a partial GraphQL query, or a full GraphQL request.
* Note: Don't forget to free the server's resources by calling the {@link WebSocketClient#stop()} method of * the returned object. * * @param * The class that is generated from the subscription definition in the GraphQL schema. It contains one * attribute, for each available subscription. The data tag of the GraphQL server response will be mapped * into an instance of this class. * @param * The type that must is returned by the subscription in the GraphQL schema, which is actually the type * that will be sent in each notification received from this subscription. * @param t * The type of the POJO which should be returned. It must be the query or the mutation class, generated * by the plugin * @param params * the input parameters for this query. If the query has no parameters, it may be null or an empty list. * @param subscriptionCallback * The object that will be called each time a message is received, or an error on the subscription * occurs. This object is provided by the application. * @param subscriptionName * The name of the subscription that should be subscribed by this method call. It will be used to check * that the correct GraphQLRequest has been provided by the caller. * @param subscriptionType * The R class * @param messageType * The T class * @return The Subscription client. It allows to stop the subscription, by executing its * {@link SubscriptionClient#unsubscribe()} method. This will stop the incoming notification flow, and will * free resources on both the client and the server. * @throws GraphQLRequestExecutionException * When an error occurs during the request execution, typically a network error, an error from the * GraphQL server or if the server response can't be parsed * @throws IOException */ public SubscriptionClient exec(Map params, SubscriptionCallback subscriptionCallback, Class subscriptionType, Class messageType) throws GraphQLRequestExecutionException { if (instanceConfiguration != null) { return instanceConfiguration.getQueryExecutor().execute(this, params, subscriptionCallback, subscriptionType, messageType); } else if (staticConfiguration != null) { return staticConfiguration.getQueryExecutor().execute(this, params, subscriptionCallback, subscriptionType, messageType); } else { throw new GraphQLRequestExecutionException( "The GraphQLRequestConfiguration has not been set in the GraphQLRequest. " + "Please set either the GraphQL instance configuration " + "or the GraphQL static configuration before executing a GraphQL request"); } } /** * Adds the __typename fields to all non scalar types * * @param graphQLRequest * @throws GraphQLRequestPreparationException */ private void addTypenameFields() throws GraphQLRequestPreparationException { // We need the __typename fields, to properly parse the JSON response for interfaces and unions. // So we add it for every returned object. if (query != null) { query.addTypenameFields(); } if (mutation != null) { mutation.addTypenameFields(); } if (subscription != null) { subscription.addTypenameFields(); } for (Fragment f : fragments) { f.addTypenameFields(); } } /** * Finish the preparation of the GraphQL request, once everything has been read: *
    *
  • add the scalar fields, for all empty non scalar fields.
  • *
  • Add the __typename field in fragments and field lists, to be sure to get it in return. This is necessary to * properly deserialize the GRaphQL interfaces and unions *
* * @throws GraphQLRequestPreparationException */ private void finishRequestPreparation() throws GraphQLRequestPreparationException { // For each non scalar field, we add its non scalar fields, if none was defined AddScalarFieldToEmptyNonScalarField(query); AddScalarFieldToEmptyNonScalarField(mutation); AddScalarFieldToEmptyNonScalarField(subscription); // Let's add the __typename fields to all non scalar types addTypenameFields(); } private void AddScalarFieldToEmptyNonScalarField(QueryField field) throws GraphQLRequestPreparationException { // If this field contains no subfield, and is not a scalar, we add all its scalar fields, as requested fields. if (field == null || field.isScalar()) { // No action } else if (field.fields.size() == 0 && field.fragments.size() == 0 && field.inlineFragments.size() == 0) { // This non scalar field has no subfields in the GraphQL request. It also have no fragment // We'll request all it scalar fields. if (field.clazz.isInterface()) { // For interfaces, we look for getters for (Method m : field.clazz.getDeclaredMethods()) { if (m.getName().startsWith("get")) { GraphQLScalar graphQLScalar = m.getAnnotation(GraphQLScalar.class); if (graphQLScalar != null) { // We've found a subfield that is a scalar. Let's add it. field.fields.add(new QueryField(field.clazz, graphQLScalar.fieldName())); } } } } else { // For objects, we look for class's attributes for (Field f : field.clazz.getDeclaredFields()) { GraphQLScalar graphQLScalar = f.getAnnotation(GraphQLScalar.class); if (graphQLScalar != null) { // We've found a subfield that is a scalar. Let's add it. field.fields.add(new QueryField(field.clazz, graphQLScalar.fieldName())); } } } } else { // This non scalar fields contains requested subfield. We recurse into each of its fields. for (QueryField f : field.fields) AddScalarFieldToEmptyNonScalarField(f); } // for } /** * * @param params * @return * @throws GraphQLRequestExecutionException */ public String buildRequest(Map params) throws GraphQLRequestExecutionException { StringBuilder sb = new StringBuilder("{\"query\":\""); // Let's start by the fragments for (Fragment fragment : fragments) { fragment.appendToGraphQLRequests(sb, params); } // Then the other parts of the request QueryField request; if (query != null) { request = query; } else if (mutation != null) { request = mutation; } else if (subscription != null) { request = subscription; } else { throw new GraphQLRequestExecutionException("[Internal error] no request has been initialized"); } // The name of the query/mutation/subscription follows special rules (including the request name and GraphQL // variables). So we need to add these things here, and not from the QueryField class. sb.append(request.name); if (requestName != null) { sb.append(" ").append(requestName); } // Let's add all GraphQL variables here StringBuilder sbGraphQLVariables = new StringBuilder(); StringBuilder sbGraphQLValues = new StringBuilder(); String separator = ""; for (InputParameter param : request.inputParameters) { if (param.getType() == InputParameterType.GRAPHQL_VARIABLE) { ////////////////////////////////////////////////////////////////////// // Let's complete the variable list, sbGraphQLVariables.append(separator)// .append("$")// .append(param.getBindParameterName())// .append(":"); // The String.repeat(int) method needs Java 11 minimum for (int i = 0; i < param.getListDepth(); i += 1) { sbGraphQLVariables.append("["); } // for sbGraphQLVariables.append(param.getGraphQLTypeName())// .append(param.isItemMandatory() ? "!" : ""); // The String.repeat(int) method needs Java 11 minimum for (int i = 0; i < param.getListDepth(); i += 1) { sbGraphQLVariables.append("]"); } // for sbGraphQLVariables.append(param.isMandatory() ? "!" : ""); ////////////////////////////////////////////////////////////////////// // And the variable value list (for the json variables field) sbGraphQLValues.append(separator)// .append("\"")// .append(param.getBindParameterName())// .append("\":")// .append(param.getValueForGraphqlQuery(true, params)); separator = ","; } } // Are there some GraphQL variables? String graphQLVariables = sbGraphQLVariables.toString(); if (graphQLVariables.length() > 0) { sb.append("(").append(graphQLVariables).append(")"); } // Let's add the whole request request.appendToGraphQLRequests(sb, params, false); // Let's finish the json string sb.append("\",\"variables\":")// .append((graphQLVariables.length() > 0) ? "{" + sbGraphQLValues + "}" : "null")// .append(",\"operationName\":null}"); return sb.toString(); } /** * This method returns the package name, where the GraphQL generated classes are. It's used to load the class * definition, and get the GraphQL metadata coming from the GraphQL schema. * * @return */ protected abstract String getGraphQLClassesPackageName(); /** * Retrieved the {@link QueryField} for the query (that is the query type coming from the GraphQL schema) from the * concrete class. * * @return * @throws GraphQLRequestPreparationException */ protected abstract QueryField getQueryContext() throws GraphQLRequestPreparationException; /** * Retrieved the {@link QueryField} for the mutation (that is the mutation type coming from the GraphQL schema) from * the concrete class. * * @return */ protected abstract QueryField getMutationContext() throws GraphQLRequestPreparationException; /** * Retrieved the {@link QueryField} for the subscription (that is the subscription type coming from the GraphQL * schema) from the concrete class. * * @return */ protected abstract QueryField getSubscriptionContext() throws GraphQLRequestPreparationException; public QueryField getQuery() { return query; } public QueryField getMutation() { return mutation; } public QueryField getSubscription() { return subscription; } public List getFragments() { return fragments; } public RequestType getRequestType() { return requestType; } public String getRequestName() { return requestName; } /** * This gets the default configuration, that will apply if no local configuration has been defined for this * instance. * * @return the staticConfiguration */ public static GraphQLConfiguration getStaticConfiguration() { return staticConfiguration; } /** * This sets the default configuration, that will apply if no local configuration has been defined for this * instance. * * @param staticConfiguration * the staticConfiguration to set */ public static void setStaticConfiguration(GraphQLConfiguration staticConfiguration) { AbstractGraphQLRequest.staticConfiguration = staticConfiguration; } /** * This gets the configuration for this instance. This configuration overrides the * {@link #getStaticConfiguration()}, if defined. * * @return the instanceConfiguration */ public GraphQLConfiguration getInstanceConfiguration() { return instanceConfiguration; } /** * This sets the configuration for this instance. This configuration overrides the * {@link #getStaticConfiguration()}, if defined. * * @param instanceConfiguration * the instanceConfiguration to set */ public void setInstanceConfiguration(GraphQLConfiguration instanceConfiguration) { this.instanceConfiguration = instanceConfiguration; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy