package com.apollographql.apollo.api;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static com.apollographql.apollo.api.internal.Utils.checkNotNull;
/**
* Field is an abstraction for a field in a graphQL operation. For example, in the following graphQL query, Field
* represents abstraction for field 'name':
*
* <pre> {@code
* {
* hero {
* name
* }
* }
* }
* </pre>
*
* Field can refer to: <b>GraphQL Scalar Types, Objects or List</b>. For a complete list of types that a Field
* object can refer to see {@link Field.Type} class.
*/
public class Field {
private final Type type;
private final String responseName;
private final String fieldName;
private final Map<String, Object> arguments;
private final boolean optional;
private static final String VARIABLE_IDENTIFIER_KEY = "kind";
private static final String VARIABLE_IDENTIFIER_VALUE = "Variable";
private static final String VARIABLE_NAME_KEY = "variableName";
/**
* Factory method for creating a Field instance representing {@link Type#STRING}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @return Field instance representing {@link Type#STRING}
*/
public static Field forString(String responseName, String fieldName, Map<String, Object> arguments,
boolean optional) {
return new Field(Type.STRING, responseName, fieldName, arguments, optional);
}
/**
* Factory method for creating a Field instance representing {@link Type#INT}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @return Field instance representing {@link Type#INT}
*/
public static Field forInt(String responseName, String fieldName, Map<String, Object> arguments, boolean optional) {
return new Field(Type.INT, responseName, fieldName, arguments, optional);
}
/**
* Factory method for creating a Field instance representing {@link Type#LONG}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @return Field instance representing {@link Type#LONG}
*/
public static Field forLong(String responseName, String fieldName, Map<String, Object> arguments,
boolean optional) {
return new Field(Type.LONG, responseName, fieldName, arguments, optional);
}
/**
* Factory method for creating a Field instance representing {@link Type#DOUBLE}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @return Field instance representing {@link Type#DOUBLE}
*/
public static Field forDouble(String responseName, String fieldName, Map<String, Object> arguments,
boolean optional) {
return new Field(Type.DOUBLE, responseName, fieldName, arguments, optional);
}
/**
* Factory method for creating a Field instance representing {@link Type#BOOLEAN}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @return Field instance representing {@link Type#BOOLEAN}
*/
public static Field forBoolean(String responseName, String fieldName, Map<String, Object> arguments,
boolean optional) {
return new Field(Type.BOOLEAN, responseName, fieldName, arguments, optional);
}
/**
* Factory method for creating a Field instance representing a custom {@link Type#OBJECT}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @param objectReader converts the field response to the custom object type
* @param <T> type of the custom object
* @return Field instance representing custom {@link Type#OBJECT}
*/
public static <T> Field forObject(String responseName, String fieldName, Map<String, Object> arguments,
boolean optional, ObjectReader<T> objectReader) {
return new ObjectField(responseName, fieldName, arguments, optional, objectReader);
}
/**
* Factory method for creating a Field instance representing {@link Type#SCALAR_LIST}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @param listReader converts the field response to a list of GraphQL scalar types
* @param <T> type of the scalar type
* @return Field instance representing {@link Type#SCALAR_LIST}
*/
public static <T> Field forList(String responseName, String fieldName, Map<String, Object> arguments,
boolean optional, ListReader<T> listReader) {
return new ScalarListField(responseName, fieldName, arguments, optional, listReader);
}
/**
* Factory method for creating a Field instance representing {@link Type#OBJECT_LIST}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @param objectReader converts the field response to a list of custom object types
* @param <T> type of the custom object
* @return Field instance representing {@link Type#OBJECT_LIST}
*/
public static <T> Field forList(String responseName, String fieldName, Map<String, Object> arguments,
boolean optional, ObjectReader<T> objectReader) {
return new ObjectListField(responseName, fieldName, arguments, optional, objectReader);
}
/**
* Factory method for creating a Field instance representing a custom GraphQL Scalar type, {@link Type#CUSTOM}
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param arguments arguments to be passed along with the field
* @param optional whether the arguments passed along are optional or required
* @param scalarType the custom scalar type of the field
* @return Field instance representing {@link Type#CUSTOM}
*/
public static Field forCustomType(String responseName, String fieldName, Map<String, Object> arguments,
boolean optional, ScalarType scalarType) {
return new CustomTypeField(responseName, fieldName, arguments, optional, scalarType);
}
/**
* Factory method for creating a Field instance representing {@link Type#CONDITIONAL}.
*
* @param responseName alias for the result of a field
* @param fieldName name of the field in the GraphQL operation
* @param conditionalTypeReader converts the field response to an optional type
* @param <T> type of the conditional
* @return Field instance representing {@link Type#CONDITIONAL}
*/
public static <T> Field forConditionalType(String responseName, String fieldName,
ConditionalTypeReader<T> conditionalTypeReader) {
return new ConditionalTypeField(responseName, fieldName, conditionalTypeReader);
}
private Field(Type type, String responseName, String fieldName, Map<String, Object> arguments, boolean optional) {
this.type = type;
this.responseName = responseName;
this.fieldName = fieldName;
this.arguments = arguments == null ? Collections.<String, Object>emptyMap()
: Collections.unmodifiableMap(arguments);
this.optional = optional;
}
public Type type() {
return type;
}
public String responseName() {
return responseName;
}
public String fieldName() {
return fieldName;
}
public Map<String, Object> arguments() {
return arguments;
}
public boolean optional() {
return optional;
}
public String cacheKey(Operation.Variables variables) {
if (arguments.isEmpty()) {
return fieldName();
}
return String.format("%s(%s)", fieldName(), orderIndependentKey(arguments, variables));
}
/**
* Resolve field argument value by name. If argument represents a references to the variable, it will be
* resolved from provided operation variables values.
*
* @param name argument name
* @param variables values of operation variables
* @return resolved argument value
*/
@SuppressWarnings("unchecked") @Nullable public Object resolveArgument(@Nonnull String name,
@Nonnull Operation.Variables variables) {
checkNotNull(name, "name == null");
checkNotNull(variables, "variables == null");
Map<String, Object> variableValues = variables.valueMap();
Object argumentValue = arguments.get(name);
if (argumentValue instanceof Map) {
Map<String, Object> argumentValueMap = (Map<String, Object>) argumentValue;
if (isArgumentValueVariableType(argumentValueMap)) {
String variableName = argumentValueMap.get(VARIABLE_NAME_KEY).toString();
return variableValues.get(variableName);
} else {
return null;
}
}
return argumentValue;
}
private String orderIndependentKey(Map<String, Object> objectMap, Operation.Variables variables) {
if (isArgumentValueVariableType(objectMap)) {
return orderIndependentKeyForVariableArgument(objectMap, variables);
}
List<Map.Entry<String, Object>> sortedArguments = new ArrayList<>(objectMap.entrySet());
Collections.sort(sortedArguments, new Comparator<Map.Entry<String, Object>>() {
@Override public int compare(Map.Entry<String, Object> argumentOne, Map.Entry<String, Object> argumentTwo) {
return argumentOne.getKey().compareTo(argumentTwo.getKey());
}
});
StringBuilder independentKey = new StringBuilder();
for (int i = 0; i < sortedArguments.size(); i++) {
Map.Entry<String, Object> argument = sortedArguments.get(i);
if (argument.getValue() instanceof Map) {
//noinspection unchecked
final Map<String, Object> objectArg = (Map<String, Object>) argument.getValue();
boolean isArgumentVariable = isArgumentValueVariableType(objectArg);
independentKey
.append(argument.getKey())
.append(":")
.append(isArgumentVariable ? "" : "[")
.append(orderIndependentKey(objectArg, variables))
.append(isArgumentVariable ? "" : "]");
} else {
independentKey.append(argument.getKey())
.append(":")
.append(argument.getValue().toString());
}
if (i < sortedArguments.size() - 1) {
independentKey.append(",");
}
}
return independentKey.toString();
}
private boolean isArgumentValueVariableType(Map<String, Object> objectMap) {
return objectMap.containsKey(VARIABLE_IDENTIFIER_KEY)
&& objectMap.get(VARIABLE_IDENTIFIER_KEY).equals(VARIABLE_IDENTIFIER_VALUE)
&& objectMap.containsKey(VARIABLE_NAME_KEY);
}
private String orderIndependentKeyForVariableArgument(Map<String, Object> objectMap, Operation.Variables variables) {
Object variable = objectMap.get(VARIABLE_NAME_KEY);
//noinspection SuspiciousMethodCalls
Object resolvedVariable = variables.valueMap().get(variable);
if (resolvedVariable == null) {
return null;
} else if (resolvedVariable instanceof Map) {
//noinspection unchecked
return orderIndependentKey((Map<String, Object>) resolvedVariable, variables);
} else {
return resolvedVariable.toString();
}
}
/**
* An abstraction for the field types
*/
public enum Type {
STRING,
INT,
LONG,
DOUBLE,
BOOLEAN,
OBJECT,
SCALAR_LIST,
OBJECT_LIST,
CUSTOM,
CONDITIONAL
}
public interface ObjectReader<T> {
T read(ResponseReader reader) throws IOException;
}
public interface ListReader<T> {
T read(ListItemReader reader) throws IOException;
}
public interface ConditionalTypeReader<T> {
T read(String conditionalType, ResponseReader reader) throws IOException;
}
public interface ListItemReader {
String readString() throws IOException;
Integer readInt() throws IOException;
Long readLong() throws IOException;
Double readDouble() throws IOException;
Boolean readBoolean() throws IOException;
<T> T readCustomType(ScalarType scalarType) throws IOException;
}
/**
* Abstraction for a Field representing a custom Object type.
*/
public static final class ObjectField extends Field {
private final ObjectReader objectReader;
ObjectField(String responseName, String fieldName, Map<String, Object> arguments, boolean optional,
ObjectReader objectReader) {
super(Type.OBJECT, responseName, fieldName, arguments, optional);
this.objectReader = objectReader;
}
public ObjectReader objectReader() {
return objectReader;
}
}
/**
* Abstraction for a Field representing a list of GraphQL scalar types.
*/
public static final class ScalarListField extends Field {
private final ListReader listReader;
ScalarListField(String responseName, String fieldName, Map<String, Object> arguments, boolean optional,
ListReader listReader) {
super(Type.SCALAR_LIST, responseName, fieldName, arguments, optional);
this.listReader = listReader;
}
public ListReader listReader() {
return listReader;
}
}
/**
* Abstraction for a Field representing a list of custom Objects.
*/
public static final class ObjectListField extends Field {
private final ObjectReader objectReader;
ObjectListField(String responseName, String fieldName, Map<String, Object> arguments, boolean optional,
ObjectReader objectReader) {
super(Type.OBJECT_LIST, responseName, fieldName, arguments, optional);
this.objectReader = objectReader;
}
public ObjectReader objectReader() {
return objectReader;
}
}
/**
* Abstraction for a Field representing a custom GraphQL scalar type.
*/
public static final class CustomTypeField extends Field {
private final ScalarType scalarType;
CustomTypeField(String responseName, String fieldName, Map<String, Object> arguments, boolean optional,
ScalarType scalarType) {
super(Type.CUSTOM, responseName, fieldName, arguments, optional);
this.scalarType = scalarType;
}
public ScalarType scalarType() {
return scalarType;
}
}
/**
* Abstraction for a Field representing a conditional type. Conditional Type is used for parsing inline fragments or
* fragments. Here is an example of how it is used:
* <pre>
* {@code
* final Field[] fields = {
* Field.forConditionalType("__typename", "__typename", new Field.ConditionalTypeReader<Fragments>() {
*
* @Override
* public Fragments read(String conditionalType, ResponseReader reader) throws IOException { return
* fragmentsFieldMapper.map(reader, conditionalType);
* }
* })
* };
* }
* </pre>
*
* In the example above, the first field '__typename' will be read and then passed to another nested mapper along with
* reader that will decide by checking conditionalType what type of fragment it will parse.
*/
public static final class ConditionalTypeField extends Field {
private final ConditionalTypeReader conditionalTypeReader;
ConditionalTypeField(String responseName, String fieldName, ConditionalTypeReader conditionalTypeReader) {
super(Type.CONDITIONAL, responseName, fieldName, null, false);
this.conditionalTypeReader = conditionalTypeReader;
}
public ConditionalTypeReader conditionalTypeReader() {
return conditionalTypeReader;
}
}
}