com.graphql_java_generator.client.request.InputParameter Maven / Gradle / Ivy
/**
*
*/
package com.graphql_java_generator.client.request;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.UUID;
import org.apache.commons.text.StringEscapeUtils;
import com.graphql_java_generator.GraphqlUtils;
import com.graphql_java_generator.annotation.GraphQLInputParameters;
import com.graphql_java_generator.annotation.GraphQLInputType;
import com.graphql_java_generator.client.GraphqlClientUtils;
import com.graphql_java_generator.client.QueryExecutorImpl;
import com.graphql_java_generator.client.directive.Directive;
import com.graphql_java_generator.exception.GraphQLRequestExecutionException;
import com.graphql_java_generator.exception.GraphQLRequestPreparationException;
import graphql.schema.GraphQLScalarType;
/**
* Contains an input parameter, to be sent to a query (mutation...). It can be either:
*
*
* A hard coded value
*
*
* A bind variable, which value must be provided when executing the query
* -
*
* @author etienne-sf
*/
public class InputParameter {
/** A utility class, that's used here */
private static GraphqlUtils graphqlUtils = new GraphqlUtils();
/** A utility class, that's used here */
private static GraphqlClientUtils graphqlClientUtils = new GraphqlClientUtils();
/** Indicates what is being read by the {@link #readTokenizerForInputParameters(StringTokenizer) method */
private enum InputParameterStep {
NAME, VALUE
};
/** The parameter name, as defined in the GraphQL schema */
final String name;
/**
* The bind parameter, as defined in the GraphQL query.
* For instance sinceParam in posts(since: :sinceParam) {date}
*/
final String bindParameterName;
/** The value to send, for this input parameter */
final Object value;
/** Indicates whether this parameter is mandatory or not */
final boolean mandatory;
/**
* If this input parameter's type is a GraphQL Custom Scalar, it is initialized in the constructor. Otherwise, it is
* null.
* graphQLCustomScalarType contains the {@link GraphQLScalarType} that allows to convert the value to a String that
* can be written in the GraphQL request, or convert from a String that is found in the GraphQL response. If this
* type is not a GraphQL Custom Scalar, it must be null.
*
*/
final GraphQLScalarType graphQLCustomScalarType;
/**
* Creates and returns a new instance of {@link InputParameter}, which is bound to a bind variable. The value for
* this bind variable must be provided, when calling the request execution.
*
* @param name
* @param bindParameterName
* The name of the bind parameter, as defined in the GraphQL response definition. It is mandator for bind
* parameters, so it may not be null here. Please read the
* client doc
* for more information on input parameters and bind variables.
* @param mandatory
* true if the parameter's value must be defined during request/mutation/subscription execution.
* If mandatory is true and the parameter's value is not provided, a
* {@link GraphQLRequestExecutionException} exception is thrown at execution time
* If mandatory is false and the parameter's value is not provided, this input parameter is not sent to
* the server
* @return
* @see QueryExecutorImpl#execute(String, ObjectResponse, List, Class)
*/
public static InputParameter newBindParameter(String name, String bindParameterName, boolean mandatory) {
if (bindParameterName == null) {
throw new NullPointerException("[Internal error] The bindParameterName is mandatory");
}
return InputParameter.newBindParameter(name, bindParameterName, mandatory, null);
}
/**
* Creates and returns a new instance of {@link InputParameter}, which is bound to a bind variable. The value for
* this bind variable must be provided, when calling the request execution.
*
* @param name
* @param bindParameterName
* The name of the bind parameter, as defined in the GraphQL response definition. It is mandator for bind
* parameters, so it may not be null here. Please read the
* client doc
* for more information on input parameters and bind variables.
* @param mandatory
* true if the parameter's value must be defined during request/mutation/subscription execution.
* If mandatory is true and the parameter's value is not provided, a
* {@link GraphQLRequestExecutionException} exception is thrown at execution time
* If mandatory is false and the parameter's value is not provided, this input parameter is not sent to
* the server
* @param graphQLCustomScalarType
* If this input parameter's type is a GraphQL Custom Scalar, it must be provided. Otherwise, it must be
* null.
* graphQLCustomScalarType contains the {@link GraphQLScalarType} that allows to convert the value to a
* String that can be written in the GraphQL request, or convert from a String that is found in the
* GraphQL response. If this type is not a GraphQL Custom Scalar, it must be null.
* @return
* @see QueryExecutorImpl#execute(String, ObjectResponse, List, Class)
*/
public static InputParameter newBindParameter(String name, String bindParameterName, boolean mandatory,
GraphQLScalarType graphQLScalarType) {
if (bindParameterName == null) {
throw new NullPointerException("[Internal error] The bindParameterName is mandatory");
}
return new InputParameter(name, bindParameterName, null, mandatory, graphQLScalarType);
}
/**
* Creates and returns a new instance of {@link InputParameter}, which value is given, and can not be changed
* afterwards
*
* @param name
* @param value
* @return
*/
public static InputParameter newHardCodedParameter(String name, Object value) {
return new InputParameter(name, null, value, true, null);
}
/**
* Creates and returns a new instance of {@link InputParameter}, which value is given, and can not be changed
* afterwards.
*
* @param name
* @param value
* @param mandatory
* @param type
* @return
*/
public static InputParameter newHardCodedParameter(String name, Object value, boolean mandatory,
GraphQLScalarType type) {
return new InputParameter(name, null, value, mandatory, type);
}
/**
* The constructor is private. Instances must be created with one of these helper methods:
* {@link #newBindParameter(String, String)} or {@link #newHardCodedParameter(String, Object)}
*
* @param name
* The parameter name, as defined in the GraphQL schema
* @param bindParameterName
* The name of the bind parameter, as defined in the GraphQL response definition. If null, it's a hard
* coded value. The value will be sent to the server, when the request is executed. Please read the
* client doc
* for more information on input parameters and bind variables.
* @param value
* The value to send, for this input parameter. If null, it's a bind parameter. The bindParameterName is
* then mandatory.
* @param mandatory
* true if the parameter's value must be defined during request/mutation/subscription execution.
* If mandatory is true and the parameter's value is not provided, a
* {@link GraphQLRequestExecutionException} exception is thrown at execution time
* If mandatory is false and the parameter's value is not provided, this input parameter is not sent to
* the server
* @param graphQLCustomScalarType
* If this input parameter's type is a GraphQL Custom Scalar, it must be provided. Otherwise, it must be
* null.
* graphQLCustomScalarType contains the {@link GraphQLScalarType} that allows to convert the value to a
* String that can be written in the GraphQL request, or convert from a String that is found in the
* GraphQL response. If this type is not a GraphQL Custom Scalar, it must be null.
*/
private InputParameter(String name, String bindParameterName, Object value, boolean mandatory,
GraphQLScalarType graphQLCustomScalarType) {
if (name == null) {
throw new NullPointerException("The input parameter's name is mandatory");
}
this.name = name;
this.bindParameterName = bindParameterName;
this.value = value;
this.mandatory = mandatory;
this.graphQLCustomScalarType = graphQLCustomScalarType;
}
/**
* Reads a list of input parameters, from a {@link QueryTokenizer}. It can be the list of parameters for a Field or
* for a Directive. It can be either a Field of a Query, Mutation or Subscription, or a Field of a standard GraphQL
* Type, or any directive...
*
* @param qt
* The StringTokenizer, where the opening parenthesis has been read. It will be read until and including
* the next closing parenthesis.
* @param directive
* if not null, then this method is reading the input parameters (arguments) for this {@link Directive}
* @param owningClass
* if not null, then this method is reading the input parameters for the field fieldName of this
* class.
* @param fieldName
* if owningClass, this is the name of the field, whose input parameters are being read.
* @param packageName
* The package name is necessary to load the generated classes, to read the metadata that has been
* generated
* @throws GraphQLRequestPreparationException
* If the request string is invalid
*/
public static List readTokenizerForInputParameters(QueryTokenizer qt, Directive directive,
Class> owningClass, String fieldName) throws GraphQLRequestPreparationException {
List ret = new ArrayList<>(); // The list that will be returned by this method
InputParameterStep step = InputParameterStep.NAME;
String parameterName = null;
while (qt.hasMoreTokens()) {
String token = qt.nextToken();
switch (token) {
case ":":
// We're about to read an input parameter value.
break;
case "{":
throw new GraphQLRequestPreparationException(
"Encountered a '{' while reading parameters for the field '" + fieldName
+ "' : if this is an input type as a parameter value, please use bind variable instead. "
+ "For instance: \"" + parameterName + ":?" + parameterName
+ "Param\", and provide a value for the " + parameterName + "Param bind parameter");
case ",":
if (step != InputParameterStep.NAME) {
throw new GraphQLRequestPreparationException("Misplaced comma for the field '" + fieldName
+ "' is not finished (no closing parenthesis)");
}
break;
case ")":
// We should be waiting for a name, and have already read at least one name
if (parameterName == null) {
throw new GraphQLRequestPreparationException("Misplaced closing parenthesis for the field '"
+ fieldName + "' (no parameter has been read)");
} else if (step != InputParameterStep.NAME) {
throw new GraphQLRequestPreparationException("Misplaced closing parenthesis for the field '"
+ fieldName + "' is not finished (no closing parenthesis)");
}
// We're finished, here.
return ret;
default:
switch (step) {
case NAME:
parameterName = token;
step = InputParameterStep.VALUE;
break;
case VALUE:
// We've read the parameter value. Let's add this parameter.
if (token.startsWith("?")) {
ret.add(new InputParameter(parameterName, token.substring(1), null, false,
graphqlClientUtils.getGraphQLType(directive, owningClass, fieldName, parameterName)));
} else if (token.startsWith("&")) {
ret.add(new InputParameter(parameterName, token.substring(1), null, true,
graphqlClientUtils.getGraphQLType(directive, owningClass, fieldName, parameterName)));
} else if (token.equals("\"")) {
// We've found a String value: let's read the string content
StringBuffer sb = new StringBuffer();
while (true) {
if (!qt.hasMoreTokens(true)) {
throw new GraphQLRequestPreparationException(
"Found the end of string before the end of the string parameter '"
+ sb.toString() + "'");
}
token = qt.nextToken(true);
if (token.contentEquals("\"")) {
// We've found the end of the string value.
break;
}
sb.append(token);
if (token.equals("\\")) {
// It's the escape character. We add the next token, as is. Especially if it's a double
// quote (as a double quote here doens't mean we found the end of the string)
sb.append(qt.nextToken(true));
}
} // while (true)
// It's a regular String.
ret.add(new InputParameter(parameterName, null, sb.toString(), true, null));
} else if (token.startsWith("\"") || token.endsWith("\"")) {
// Too bad, there is a " only at the end or only at the beginning
throw new GraphQLRequestPreparationException(
"Bad parameter value: parameter values should start and finish by \", or not having any \" at the beginning and end."
+ " But it's not the case for the value <" + token + "> of parameter <"
+ parameterName
+ ">. Maybe you wanted to add a bind parameter instead (bind parameter must start with a ? or a &");
} else if (directive != null) {
Object parameterValue = parseDirectiveArgumentValue(directive, parameterName, token);
InputParameter arg = new InputParameter(parameterName, null, parameterValue, true, null);
ret.add(arg);
directive.getArguments().add(arg);
} else {
Object parameterValue = parseInputParameterValue(owningClass, fieldName, parameterName, token);
ret.add(new InputParameter(parameterName, null, parameterValue, true, null));
}
step = InputParameterStep.NAME;
break;
}
}// switch (token)
} // while (st.hasMoreTokens())
throw new GraphQLRequestPreparationException(
"The list of parameters for the field '" + fieldName + "' is not finished (no closing parenthesis)");
}
/**
* Parse a value read for an input parameter, within the query
*
* @param owningClass
* @param fieldName
* @param parameterName
* @param parameterValue
* @return
* @throws GraphQLRequestPreparationException
*/
private static Object parseInputParameterValue(Class> owningClass, String fieldName, String parameterName,
String parameterValue) throws GraphQLRequestPreparationException {
Field field = graphqlUtils.getDeclaredField(owningClass, graphqlUtils.getJavaName(fieldName), true);
GraphQLInputParameters graphQLInputParameters = field.getDeclaredAnnotation(GraphQLInputParameters.class);
if (graphQLInputParameters == null) {
throw new GraphQLRequestPreparationException(
"[Internal error] The field '" + fieldName + "' is lacking the GraphQLInputParameters annotation");
}
for (int i = 0; i < graphQLInputParameters.names().length; i += 1) {
if (graphQLInputParameters.names()[i].equals(parameterName)) {
// We've found the parameterType. Let's get its value.
try {
return parseValueForInputParameter(parameterValue, graphQLInputParameters.types()[i],
owningClass.getPackage().getName());
} catch (Exception e) {
throw new GraphQLRequestPreparationException(
"Could not read the value for the parameter '" + parameterName + "' of the field '"
+ fieldName + "' of the type '" + owningClass.getName() + "'");
}
}
}
// Too bad...
throw new GraphQLRequestPreparationException("[Internal error] Can't find the type for the parameter '"
+ parameterName + "' of the field '" + fieldName + "'");
}
private static Object parseDirectiveArgumentValue(Directive directive, String parameterName, String parameterValue)
throws GraphQLRequestPreparationException {
// Let's find the directive definition for this read directive
Directive directiveDefinition = directive.getDirectiveDefinition();
// Let's find the parameter type, so that we can call parseValueForInputParameter method
for (InputParameter param : directiveDefinition.getArguments()) {
if (param.getName().equals(parameterName)) {
// We've found the parameterType. Let's get its value.
try {
return parseValueForInputParameter(parameterValue, param.getGraphQLScalarType().getName(),
directive.getPackageName());
} catch (Exception e) {
throw new GraphQLRequestPreparationException("Could not read the value for the parameter '"
+ parameterName + "' of the directive '" + directive.getName() + "'", e);
}
}
}
// Too bad...
throw new GraphQLRequestPreparationException("[Internal error] Can't find the argument '" + parameterName
+ "' of the directive '" + directive.getName() + "'");
}
/**
* Parse a value, depending on the parameter type.
*
* @param parameterValue
* @param parameterType
* @param packageName
* @return
* @throws GraphQLRequestPreparationException
*/
private static Object parseValueForInputParameter(String parameterValue, String parameterType, String packageName)
throws GraphQLRequestPreparationException {
try {
return graphqlUtils.parseValueForInputParameter(parameterValue, parameterType,
graphqlUtils.getClass(packageName, parameterType));
} catch (RuntimeException e) {
throw new GraphQLRequestPreparationException(e.getMessage(), e);
}
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
/**
* Returns the parameter, as it should be written in the GraphQL query. For instance:
*
* - String: a "string" -> "a \"string\""
* - Enum: EPISODE -> EPISODE (no escape or double quote here)
*
*
* @param bindVariables
* The map for the bind variables. It may be null, if this input parameter is a hard coded one. If this
* parameter is a Bind Variable, then bindVariables is mandatory, and it must contain a value for th bind
* parameter which name is stored in {@link #bindParameterName}.
* @return
* @throws GraphQLRequestExecutionException
*/
public String getValueForGraphqlQuery(Map bindVariables) throws GraphQLRequestExecutionException {
if (this.bindParameterName == null) {
// It's a hard coded value
return this.getValueForGraphqlQuery(this.value, graphQLCustomScalarType);
} else
// It's a Bind Variable.
// If the InputParameter is mandatory, which must have its value in the map of BindVariables.
if (mandatory && (bindVariables == null || !bindVariables.keySet().contains(this.bindParameterName))) {
throw new GraphQLRequestExecutionException("The Bind Parameter for '" + this.bindParameterName
+ "' must be provided in the BindVariables map");
}
if (bindVariables == null || !bindVariables.keySet().contains(this.bindParameterName))
return null;
else
return this.getValueForGraphqlQuery(bindVariables.get(this.bindParameterName), graphQLCustomScalarType);
}
/**
* This method is used both by {@link #getValueForGraphqlQuery()} and {@link #getListValue(List)} to extract a value
* as a string.
*
* @param val
* This value of the parameter. It can be the {@link #value} if it is not null, or the binding from the
* bind parameters. It's up to the caller to map the bind parameter into this method argument.
* @return
* @throws GraphQLRequestExecutionException
*/
String getValueForGraphqlQuery(Object val, GraphQLScalarType graphQLScalarType)
throws GraphQLRequestExecutionException {
if (val == null) {
return null;
} else if (val instanceof java.util.List) {
return getListValue((List>) val, graphQLScalarType);
} else if (graphQLScalarType != null) {
Object ret = graphQLScalarType.getCoercing().serialize(val);
if (ret instanceof String)
return getStringValue((String) ret);
else
return ret.toString();
} else if (val instanceof String) {
return getStringValue((String) val);
} else if (val instanceof UUID) {
return getStringValue(((UUID) val).toString());
} else if (val.getClass().getAnnotation(GraphQLInputType.class) != null) {
return getInputTypeStringValue(val);
} else {
return val.toString();
}
}
/**
* Escape a string in accordance with the rules defined for JSON strings so that it can be included in a GraphQL
* payload. Because a GraphQL request consists of stringified JSON objects wrapped in another JSON object, the
* escaping is applied twice.
*
* @see json.org section on strings
* @return escaped string
*/
private String getStringValue(String str) {
return "\\\"" + StringEscapeUtils.escapeJson(StringEscapeUtils.escapeJson(str)) + "\\\"";
}
/**
* This method returns the JSON string that represents the given list, according to GraphQL standard. This method is
* used to write a part of the GraphQL client query that will be sent to the server.
*
* @param list
* a non null List
* @return
* @throws GraphQLRequestExecutionException
* @throws NullPointerException
* If lst is null
*/
private String getListValue(List> list, GraphQLScalarType graphQLScalarType)
throws GraphQLRequestExecutionException {
StringBuilder result = new StringBuilder("[");
for (int index = 0; index < list.size(); index++) {
Object obj = list.get(index);
result.append(this.getValueForGraphqlQuery(obj, graphQLScalarType));
if (index < list.size() - 1) {
result.append(",");
}
}
return result.append("]").toString();
}
/**
* This method returns the JSON string that represents the given object, according to GraphQL standard. This method
* is used to write a part of the GraphQL client query that will be sent to the server.
*
* @param object
* An object which class is an InputType as defined in the GraphQL schema
* @return The String that represents this object, according to GraphQL standard representation, as expected in the
* query to be sent to the server
*/
private String getInputTypeStringValue(Object object) throws GraphQLRequestExecutionException {
StringBuilder result = new StringBuilder("{");
String separator = "";
for (Field field : object.getClass().getDeclaredFields()) {
Object val = graphqlUtils.invokeGetter(object, field.getName());
if (val != null) {
result.append(separator);
result.append(field.getName());
result.append(":");
result.append(getValueForGraphqlQuery(val, graphqlClientUtils.getGraphQLCustomScalarType(field)));
separator = ",";
}
} // for
return result.append("}").toString();
}
public String getBindParameterName() {
return bindParameterName;
}
public boolean isMandatory() {
return mandatory;
}
public GraphQLScalarType getGraphQLScalarType() {
return graphQLCustomScalarType;
}
public static void appendInputParametersToGraphQLRequests(StringBuilder sb, List inputParameters,
Map parameters) throws GraphQLRequestExecutionException {
if (inputParameters != null && inputParameters.size() > 0) {
// Let's list the non null parameters ...
List params = new ArrayList();
for (InputParameter param : inputParameters) {
String stringValue = param.getValueForGraphqlQuery(parameters);
if (stringValue != null) {
params.add(param.getName() + ":" + stringValue);
}
}
// ... in order to generate the list of parameters to send to the server
if (params.size() > 0) {
sb.append("(");
boolean writeComma = false;
for (String param : params) {
if (writeComma)
sb.append(",");
writeComma = true;
sb.append(param);
} // for
sb.append(")");
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy