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

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

There is a newer version: 2.0RC1
Show newest version
package com.graphql_java_generator.client.request;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import com.graphql_java_generator.annotation.GraphQLScalar;
import com.graphql_java_generator.client.GraphqlUtils;
import com.graphql_java_generator.client.response.GraphQLRequestPreparationException;

/**
 * This class is a Builder that'll help to build a {@link ObjectResponse}, which defines what should appear in the
 * response from the GraphQL server.
 * 
 * @author EtienneSF
 */
public class Builder {

	GraphqlUtils graphqlUtils = new GraphqlUtils();

	final ObjectResponse objectResponse;

	class QueryField {
		/** The name of this field */
		String name;
		/** The alias of this field */
		String alias;
		/**
		 * All subfields contained in this field. Empty if the field is a GraphQL Scalar. At least one if the field is a
		 * not a Scalar
		 */
		List fields = new ArrayList<>();

		QueryField(String name) {
			this.name = name;
		}

		public void readTokenizer(StringTokenizer st) throws GraphQLRequestPreparationException {
			// The last field we've read.
			QueryField lastReadField = null;

			while (st.hasMoreTokens()) {
				String token = st.nextToken();
				switch (token) {
				case " ":
					// Nothing to do.
					break;
				case ":":
					// The previously read field name is actually an alias
					if (lastReadField == null) {
						throw new GraphQLRequestPreparationException(
								"The given query has a ':' character, not preceded by a proper alias name (before <"
										+ st.nextToken() + ">)");
					}
					lastReadField.alias = lastReadField.name;
					// The real field name is the next real token (we'll check latter that the field names are valid)
					lastReadField.name = " ";
					while (lastReadField.name.equals(" ")) {
						lastReadField.name = st.nextToken();
					}
					break;
				case "(":
					throw new GraphQLRequestPreparationException(
							"The given query contains a '(' (parenthesis), field parameters are not managed yet");
				case "{":
					// The last field we've read is actually an object (a non Scalar GraphQL type), as it itself has
					// fields
					if (lastReadField == null) {
						throw new GraphQLRequestPreparationException(
								"The given query has two '{', one after another (error while reading field <" + name
										+ ">) while reading <" + this.name + ">");
					} else if (lastReadField.fields.size() > 0) {
						throw new GraphQLRequestPreparationException(
								"The given query contains a '{' not preceded by a fieldname, after field <"
										+ lastReadField.name + "> while reading <" + this.name + ">");
					} else {
						// Ok, let's read the field for the subobject, for which we just read the name (and potentiel
						// alias :
						lastReadField.readTokenizer(st);
						// Let's clear the lastReadField, as we already have read its content.
						lastReadField = null;
					}
					break;
				case "}":
					// We're finished our current object : let's get out of this method.
					return;
				default:
					// It's a field. Scalar or not ? That is the question. We don't care yet. If the next token is a
					// '{', we'll read its content and fill its fields list.
					lastReadField = new QueryField(token);
					fields.add(lastReadField);
				}// switch
			} // while

			// Oups, we should not arrive here :
			throw new GraphQLRequestPreparationException("The field <" + name
					+ "> has a non finished list of fields (it lacks the finishing '}') while reading <" + this.name
					+ ">");
		}
	}

	Builder(Class objectClass) {
		objectResponse = new ObjectResponse(objectClass);
	}

	/**
	 * Adds a scalar field with no alias, to the {@link ObjectResponse} we are building
	 * 
	 * @param fieldName
	 * @return The current builder, to allow the standard builder construction chain
	 * @throws NullPointerException
	 *             If the fieldName is null
	 * @throws GraphQLRequestPreparationException
	 *             If the fieldName or the fieldAlias is not valid
	 */
	public Builder withField(String fieldName) throws GraphQLRequestPreparationException {
		return withField(fieldName, null);
	}

	/**
	 * Adds a scalar field with an alias, to the {@link ObjectResponse} we are building
	 * 
	 * @param fieldName
	 * @param alias
	 * @return The current builder, to allow the standard builder construction chain
	 * @throws NullPointerException
	 *             If the fieldName is null
	 * @throws GraphQLRequestPreparationException
	 *             If the fieldName or the fieldAlias is not valid
	 */
	public Builder withField(String fieldName, String alias) throws GraphQLRequestPreparationException {
		// We check that this field exist, and is a scaler
		graphqlUtils.checkFieldOfGraphQLType(fieldName, true, objectResponse.fieldClass);

		// Let's check that this field is not already in the list
		for (ObjectResponse.Field field : objectResponse.scalarFields) {
			if (field.name.equals(fieldName)) {
				throw new GraphQLRequestPreparationException("The field <" + fieldName
						+ "> is already in the field list for the objet <" + objectResponse.fieldName + ">");
			}
		}

		// This will check that the alias is null or a valid GraphQL identifier
		objectResponse.scalarFields.add(new ObjectResponse.Field(fieldName, alias));
		return this;
	}

	/**
	 * Adds a non scalar field (a subobject) without alias, to the {@link ObjectResponse} we are building
	 * 
	 * @param fieldName
	 * @param alias
	 * @return The current builder, to allow the standard builder construction chain
	 * @throws NullPointerException
	 *             If the fieldName is null
	 * @throws GraphQLRequestPreparationException
	 *             If the fieldName or the fieldAlias is not valid
	 */
	public Builder withSubObject(String fieldName, ObjectResponse objectResponse)
			throws GraphQLRequestPreparationException {
		return withSubObject(fieldName, null, objectResponse);
	}

	/**
	 * Adds a scalar field (a subobject) with an alias, to the {@link ObjectResponse} we are building
	 * 
	 * @param fieldName
	 * @param alias
	 * @return The current builder, to allow the standard builder construction chain
	 * @throws NullPointerException
	 *             If the fieldName is null
	 * @throws GraphQLRequestPreparationException
	 *             If the fieldName or the fieldAlias is not valid
	 */
	public Builder withSubObject(String fieldName, String fieldAlias, ObjectResponse subobjetResponseDef)
			throws GraphQLRequestPreparationException {

		// The subobject is identified by the given fieldName in the fieldClass of the current objetResponseDef.
		// Let's check that the responseDefParam is of the good class.
		Class subObjetClass = graphqlUtils.checkFieldOfGraphQLType(fieldName, false, objectResponse.fieldClass);
		if (!subObjetClass.equals(subobjetResponseDef.fieldClass)) {
			throw new GraphQLRequestPreparationException("Error creating subobject: the given field <" + fieldName
					+ "> is of type " + subObjetClass.getName() + ", but the given ObjetResponseDef is of type "
					+ subobjetResponseDef.fieldClass.getName());
		}

		// Let's check that this subobject is not already in the list
		for (ObjectResponse subObject : objectResponse.subObjects) {
			if (subObject.fieldName.equals(fieldName)) {
				throw new GraphQLRequestPreparationException("The field <" + subObject.fieldName
						+ "> is already in the field list for the objet <" + objectResponse.fieldName + ">");
			}
		}

		// Ok, s let's create the subobject
		subobjetResponseDef.setOwningClass(this.objectResponse.fieldClass);
		subobjetResponseDef.setField(fieldName, fieldAlias);

		// Then, we register this objectResponse as a subObject
		this.objectResponse.subObjects.add(subobjetResponseDef);

		// Let's go on with our builder
		return this;
	}

	/**
	 * Returns the built {@link ObjectResponse}. If no field (either scalar or suboject) has been added, then all scalar
	 * fields are added.
	 * 
	 * @return
	 * @throws GraphQLRequestPreparationException
	 */
	public ObjectResponse build() throws GraphQLRequestPreparationException {
		// If no field (either scalar or suboject) has been added, then all scalar fields are added.
		if (objectResponse.scalarFields.size() == 0 && objectResponse.subObjects.size() == 0) {
			addKnownScalarFields();
		}
		return objectResponse;
	}

	/**
	 * Builds a {@link ObjectResponse} from a part of a GraphQL query. This part define what's expected as a response
	 * for the field of the current {@link ObjectResponse} for this builder.
	 * 
	 * @param queryResponseDef
	 *            A part of a response, for instance (for the hero query of the Star Wars GraphQL schema): "{ id name
	 *            friends{name}}"
* No special character are allowed (linefeed...).
* This parameter can be a null or an empty string. In this case, all scalar fields are added. * @param episode * @return * @throws GraphQLRequestPreparationException */ public Builder withQueryResponseDef(String queryResponseDef) throws GraphQLRequestPreparationException { if (queryResponseDef == null || queryResponseDef.trim().equals("")) { addKnownScalarFields(); } else { // Ok, we have to parse a string which looks like that: "{ id name friends{name}}" // We first replace each "{" by " { " and "}" by " } ". Then we tokenize the string, by using the space as a // delimiter StringTokenizer st = new StringTokenizer(queryResponseDef, " {}:()", true); // We expect a first "{" String token = " "; while (token.equals(" ")) { token = st.nextToken(); } if (!token.equals("{")) { throw new GraphQLRequestPreparationException("The queryResponseDef should start with '{'"); } QueryField queryField = new QueryField(objectResponse.fieldName); try { queryField.readTokenizer(st); } catch (GraphQLRequestPreparationException e) { throw new GraphQLRequestPreparationException( e.getMessage() + " while reading the queryReponseDef: " + queryResponseDef, e); } // We should have only spaces left while (st.hasMoreTokens()) { token = st.nextToken(); switch (token) { case " ": // Nothing to do. break; default: throw new GraphQLRequestPreparationException( "Unexpected token <" + token + "> at the end of the queryReponseDef: " + queryResponseDef); }// switch } // while // Ok, the queryResponseDef has been parsed, and the content is store in our queryField. // Let's build our ObjectResponse withQueryField(queryField); } return this; } /** * Add all scalar fields of the current class into the current {@link ObjectResponse}. The scalar fields which have * already been added to the query are not added, just in case. * * @throws GraphQLRequestPreparationException * */ private void addKnownScalarFields() throws GraphQLRequestPreparationException { if (objectResponse.getFieldClass().isInterface()) { // For interfaces, we loop through all getters for (Method method : objectResponse.getFieldClass().getDeclaredMethods()) { if (method.getName().startsWith("get")) { GraphQLScalar annotation = method.getAnnotation(GraphQLScalar.class); if (annotation != null) { // Ok, we have a getter (like getName), annotated by GraphQLNonScalar withField(getCamelCase(method.getName().substring(3))); } } } } else { // For classes, we loop through all attributes for (java.lang.reflect.Field attribute : objectResponse.getFieldClass().getDeclaredFields()) { GraphQLScalar annotation = attribute.getAnnotation(GraphQLScalar.class); if (annotation != null) { // Ok, we have a getter (like getName), annotated by GraphQLNonScalar withField(getCamelCase(attribute.getName())); } } } } /** * Convert the given name, to a camel case name. Currenly very simple : it puts the first character in lower case. * * @return */ public static String getCamelCase(String name) { return name.substring(0, 1).toLowerCase() + name.substring(1); } /** * Reads the fields contained in the given {@link QueryField}, and call the relevant withXxx method of this builder * * @param queryField * @throws GraphQLRequestPreparationException */ private Builder withQueryField(QueryField queryField) throws GraphQLRequestPreparationException { if (!queryField.name.equals(objectResponse.getFieldName())) { throw new GraphQLRequestPreparationException("[INTERNAL ERROR] the field name of the queryField is <" + queryField.name + "> whereas the field name of the objetResponseDef is <" + objectResponse.getFieldName() + ">"); } for (QueryField field : queryField.fields) { if (field.fields.size() == 0) { // It's a Scalar withField(field.name, field.alias); } else { // It's a non Scalar field : we'll recurse down one level, by calling withQueryField again. ObjectResponse subobjectResponseDef = ObjectResponse .newQueryResponseDefBuilder(objectResponse.fieldClass, field.name, field.alias) .withQueryField(field).build(); withSubObject(field.name, field.alias, subobjectResponseDef); } } return this; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy