ExecutionContext.java
package graphql.execution;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import graphql.EngineRunningState;
import graphql.ExecutionInput;
import graphql.ExperimentalApi;
import graphql.GraphQLContext;
import graphql.GraphQLError;
import graphql.Internal;
import graphql.Profiler;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
import graphql.execution.incremental.IncrementalCallState;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.language.Document;
import graphql.language.FragmentDefinition;
import graphql.language.OperationDefinition;
import graphql.normalized.ExecutableNormalizedOperation;
import graphql.normalized.ExecutableNormalizedOperationFactory;
import graphql.schema.GraphQLSchema;
import graphql.util.FpKit;
import graphql.util.LockKit;
import org.dataloader.DataLoaderRegistry;
import org.jspecify.annotations.Nullable;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
@SuppressWarnings("TypeParameterUnusedInFormals")
@PublicApi
public class ExecutionContext {
private final GraphQLSchema graphQLSchema;
private final ExecutionId executionId;
private final InstrumentationState instrumentationState;
private final ExecutionStrategy queryStrategy;
private final ExecutionStrategy mutationStrategy;
private final ExecutionStrategy subscriptionStrategy;
private final ImmutableMap<String, FragmentDefinition> fragmentsByName;
private final OperationDefinition operationDefinition;
private final Document document;
private final CoercedVariables coercedVariables;
private final Supplier<NormalizedVariables> normalizedVariables;
private final Object root;
private final Object context;
private final GraphQLContext graphQLContext;
private final Object localContext;
private final Instrumentation instrumentation;
private final AtomicReference<ImmutableList<GraphQLError>> errors = new AtomicReference<>(ImmutableKit.emptyList());
private final LockKit.ReentrantLock errorsLock = new LockKit.ReentrantLock();
private final Set<ResultPath> errorPaths = new HashSet<>();
private final DataLoaderRegistry dataLoaderRegistry;
private final Locale locale;
private final IncrementalCallState incrementalCallState = new IncrementalCallState();
private final ValueUnboxer valueUnboxer;
private final ResponseMapFactory responseMapFactory;
private final ExecutionInput executionInput;
private final Supplier<ExecutableNormalizedOperation> queryTree;
private final boolean propagateErrorsOnNonNullContractFailure;
// this is modified after creation so it needs to be volatile to ensure visibility across Threads
private volatile DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP;
private final ResultNodesInfo resultNodesInfo = new ResultNodesInfo();
private final EngineRunningState engineRunningState;
private final Profiler profiler;
ExecutionContext(ExecutionContextBuilder builder) {
this.graphQLSchema = builder.graphQLSchema;
this.executionId = builder.executionId;
this.instrumentationState = builder.instrumentationState;
this.queryStrategy = builder.queryStrategy;
this.mutationStrategy = builder.mutationStrategy;
this.subscriptionStrategy = builder.subscriptionStrategy;
this.fragmentsByName = builder.fragmentsByName;
this.coercedVariables = builder.coercedVariables;
this.normalizedVariables = builder.normalizedVariables;
this.document = builder.document;
this.operationDefinition = builder.operationDefinition;
this.context = builder.context;
this.graphQLContext = builder.graphQLContext;
this.root = builder.root;
this.instrumentation = builder.instrumentation;
this.dataLoaderRegistry = builder.dataLoaderRegistry;
this.locale = builder.locale;
this.valueUnboxer = builder.valueUnboxer;
this.responseMapFactory = builder.responseMapFactory;
this.errors.set(builder.errors);
this.localContext = builder.localContext;
this.executionInput = builder.executionInput;
this.dataLoaderDispatcherStrategy = builder.dataLoaderDispatcherStrategy;
this.queryTree = FpKit.interThreadMemoize(() -> ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, operationDefinition, fragmentsByName, coercedVariables));
this.propagateErrorsOnNonNullContractFailure = builder.propagateErrorsOnNonNullContractFailure;
this.engineRunningState = builder.engineRunningState;
this.profiler = builder.profiler;
}
public ExecutionId getExecutionId() {
return executionId;
}
public ExecutionInput getExecutionInput() {
return executionInput;
}
public InstrumentationState getInstrumentationState() {
return instrumentationState;
}
public Instrumentation getInstrumentation() {
return instrumentation;
}
public GraphQLSchema getGraphQLSchema() {
return graphQLSchema;
}
public Map<String, FragmentDefinition> getFragmentsByName() {
return fragmentsByName;
}
public Document getDocument() {
return document;
}
public OperationDefinition getOperationDefinition() {
return operationDefinition;
}
public CoercedVariables getCoercedVariables() {
return coercedVariables;
}
/**
* @return a supplier that will give out the operations variables in normalized form
*/
public Supplier<NormalizedVariables> getNormalizedVariables() {
return normalizedVariables;
}
/**
* @param <T> for two
*
* @return the legacy context
*
* @deprecated use {@link #getGraphQLContext()} instead
*/
@Deprecated(since = "2021-07-05")
@SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
public <T> T getContext() {
return (T) context;
}
public GraphQLContext getGraphQLContext() {
return graphQLContext;
}
@SuppressWarnings("unchecked")
public <T> T getLocalContext() {
return (T) localContext;
}
@SuppressWarnings("unchecked")
public <T> T getRoot() {
return (T) root;
}
public FragmentDefinition getFragment(String name) {
return fragmentsByName.get(name);
}
public DataLoaderRegistry getDataLoaderRegistry() {
return dataLoaderRegistry;
}
public Locale getLocale() {
return locale;
}
public ValueUnboxer getValueUnboxer() {
return valueUnboxer;
}
/**
* @return true if the current operation should propagate errors in non-null positions
* Propagating errors is the default. Error aware clients may opt in returning null in non-null positions
* by using the `@experimental_disableErrorPropagation` directive.
*
* @see graphql.Directives#setExperimentalDisableErrorPropagationEnabled(boolean) to change the JVM wide default
*/
@ExperimentalApi
public boolean propagateErrorsOnNonNullContractFailure() {
return propagateErrorsOnNonNullContractFailure;
}
/**
* @return true if the current operation is a Query
*/
public boolean isQueryOperation() {
return isOpType(OperationDefinition.Operation.QUERY);
}
/**
* @return true if the current operation is a Mutation
*/
public boolean isMutationOperation() {
return isOpType(OperationDefinition.Operation.MUTATION);
}
/**
* @return true if the current operation is a Subscription
*/
public boolean isSubscriptionOperation() {
return isOpType(OperationDefinition.Operation.SUBSCRIPTION);
}
private boolean isOpType(OperationDefinition.Operation operation) {
if (operationDefinition != null) {
return operation.equals(operationDefinition.getOperation());
}
return false;
}
/**
* This method will only put one error per field path.
*
* @param error the error to add
* @param fieldPath the field path to put it under
*/
public void addError(GraphQLError error, ResultPath fieldPath) {
errorsLock.runLocked(() -> {
//
// see https://spec.graphql.org/October2021/#sec-Handling-Field-Errors about how per
// field errors should be handled - ie only once per field if it's already there for nullability
// but unclear if it's not that error path
//
if (!errorPaths.add(fieldPath)) {
return;
}
this.errors.set(ImmutableKit.addToList(this.errors.get(), error));
});
}
/**
* This method will allow you to add errors into the running execution context, without a check
* for per field unique-ness
*
* @param error the error to add
*/
public void addError(GraphQLError error) {
errorsLock.runLocked(() -> {
// see https://github.com/graphql-java/graphql-java/issues/888 on how the spec is unclear
// on how exactly multiple errors should be handled - ie only once per field or not outside the nullability
// aspect.
if (error.getPath() != null) {
ResultPath path = ResultPath.fromList(error.getPath());
this.errorPaths.add(path);
}
this.errors.set(ImmutableKit.addToList(this.errors.get(), error));
});
}
/**
* This method will allow you to add errors into the running execution context, without a check
* for per field unique-ness
*
* @param errors the errors to add
*/
public void addErrors(List<GraphQLError> errors) {
if (errors.isEmpty()) {
return;
}
// we are locked because we set two fields at once - but we only ever read one of them later
// in getErrors so no need for synchronised there.
errorsLock.runLocked(() -> {
Set<ResultPath> newErrorPaths = new HashSet<>();
for (GraphQLError error : errors) {
// see https://github.com/graphql-java/graphql-java/issues/888 on how the spec is unclear
// on how exactly multiple errors should be handled - ie only once per field or not outside the nullability
// aspect.
if (error.getPath() != null) {
ResultPath path = ResultPath.fromList(error.getPath());
newErrorPaths.add(path);
}
}
this.errorPaths.addAll(newErrorPaths);
this.errors.set(ImmutableKit.concatLists(this.errors.get(), errors));
});
}
@Internal
public ResponseMapFactory getResponseMapFactory() {
return responseMapFactory;
}
/**
* @return the total list of errors for this execution context
*/
public List<GraphQLError> getErrors() {
return errors.get();
}
public ExecutionStrategy getQueryStrategy() {
return queryStrategy;
}
public ExecutionStrategy getMutationStrategy() {
return mutationStrategy;
}
public ExecutionStrategy getSubscriptionStrategy() {
return subscriptionStrategy;
}
public IncrementalCallState getIncrementalCallState() {
return incrementalCallState;
}
public ExecutionStrategy getStrategy(OperationDefinition.Operation operation) {
if (operation == OperationDefinition.Operation.MUTATION) {
return getMutationStrategy();
} else if (operation == OperationDefinition.Operation.SUBSCRIPTION) {
return getSubscriptionStrategy();
} else {
return getQueryStrategy();
}
}
public Supplier<ExecutableNormalizedOperation> getNormalizedQueryTree() {
return queryTree;
}
@Internal
public void setDataLoaderDispatcherStrategy(DataLoaderDispatchStrategy dataLoaderDispatcherStrategy) {
this.dataLoaderDispatcherStrategy = dataLoaderDispatcherStrategy;
}
@Internal
public DataLoaderDispatchStrategy getDataLoaderDispatcherStrategy() {
return dataLoaderDispatcherStrategy;
}
/**
* This helps you transform the current ExecutionContext object into another one by starting a builder with all
* the current values and allows you to transform it how you want.
*
* @param builderConsumer the consumer code that will be given a builder to transform
*
* @return a new ExecutionContext object based on calling build on that builder
*/
public ExecutionContext transform(Consumer<ExecutionContextBuilder> builderConsumer) {
ExecutionContextBuilder builder = ExecutionContextBuilder.newExecutionContextBuilder(this);
builderConsumer.accept(builder);
return builder.build();
}
public ResultNodesInfo getResultNodesInfo() {
return resultNodesInfo;
}
@Internal
public boolean hasIncrementalSupport() {
GraphQLContext graphqlContext = getGraphQLContext();
return graphqlContext != null && graphqlContext.getBoolean(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT);
}
@Internal
public EngineRunningState getEngineRunningState() {
return engineRunningState;
}
@Internal
@Nullable
Throwable possibleCancellation(@Nullable Throwable currentThrowable) {
return engineRunningState.possibleCancellation(currentThrowable);
}
@Internal
public Profiler getProfiler() {
return profiler;
}
@Internal
void throwIfCancelled() throws AbortExecutionException {
engineRunningState.throwIfCancelled();
}
}