com.apollographql.apollo3.runtime.java.ApolloClient Maven / Gradle / Ivy
Show all versions of apollo-runtime-java Show documentation
package com.apollographql.apollo3.runtime.java;
import com.apollographql.apollo3.api.Adapter;
import com.apollographql.apollo3.api.ApolloRequest;
import com.apollographql.apollo3.api.CustomScalarAdapters;
import com.apollographql.apollo3.api.CustomScalarType;
import com.apollographql.apollo3.api.ExecutionContext;
import com.apollographql.apollo3.api.ExecutionOptions;
import com.apollographql.apollo3.api.MutableExecutionOptions;
import com.apollographql.apollo3.api.Mutation;
import com.apollographql.apollo3.api.Operation;
import com.apollographql.apollo3.api.Query;
import com.apollographql.apollo3.api.ScalarType;
import com.apollographql.apollo3.api.Subscription;
import com.apollographql.apollo3.api.http.DefaultHttpRequestComposer;
import com.apollographql.apollo3.api.http.HttpHeader;
import com.apollographql.apollo3.api.http.HttpMethod;
import com.apollographql.apollo3.runtime.java.interceptor.ApolloInterceptor;
import com.apollographql.apollo3.runtime.java.interceptor.ApolloInterceptorChain;
import com.apollographql.apollo3.runtime.java.interceptor.internal.AutoPersistedQueryInterceptor;
import com.apollographql.apollo3.runtime.java.interceptor.internal.DefaultInterceptorChain;
import com.apollographql.apollo3.runtime.java.internal.DefaultApolloCall;
import com.apollographql.apollo3.runtime.java.internal.DefaultApolloDisposable;
import com.apollographql.apollo3.runtime.java.network.NetworkTransport;
import com.apollographql.apollo3.runtime.java.network.http.HttpEngine;
import com.apollographql.apollo3.runtime.java.network.http.HttpInterceptor;
import com.apollographql.apollo3.runtime.java.network.http.HttpNetworkTransport;
import com.apollographql.apollo3.runtime.java.network.http.internal.BatchingHttpInterceptor;
import com.apollographql.apollo3.runtime.java.network.http.internal.OkHttpHttpEngine;
import com.apollographql.apollo3.runtime.java.network.ws.WebSocketNetworkTransport;
import com.apollographql.apollo3.runtime.java.network.ws.protocol.GraphQLWsProtocol;
import com.apollographql.apollo3.runtime.java.network.ws.protocol.WsProtocol;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.WebSocket;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import static com.apollographql.apollo3.api.java.Assertions.checkNotNull;
import static java.util.concurrent.Executors.newCachedThreadPool;
public class ApolloClient implements Closeable {
private Executor executor;
private List interceptors;
private CustomScalarAdapters customScalarAdapters;
private NetworkInterceptor networkInterceptor;
private HttpMethod httpMethod;
private List httpHeaders;
private Boolean sendApqExtensions;
private Boolean sendDocument;
private Boolean enableAutoPersistedQueries;
private Boolean canBeBatched;
private ApolloClient(
Executor executor,
NetworkTransport httpNetworkTransport,
NetworkTransport subscriptionNetworkTransport,
List interceptors,
CustomScalarAdapters customScalarAdapters,
HttpMethod httpMethod,
List httpHeaders,
Boolean sendApqExtensions,
Boolean sendDocument,
Boolean enableAutoPersistedQueries,
Boolean canBeBatched
) {
this.executor = executor;
this.interceptors = interceptors;
this.customScalarAdapters = customScalarAdapters;
this.httpMethod = httpMethod;
this.httpHeaders = httpHeaders;
this.sendApqExtensions = sendApqExtensions;
this.sendDocument = sendDocument;
this.enableAutoPersistedQueries = enableAutoPersistedQueries;
this.canBeBatched = canBeBatched;
networkInterceptor = new NetworkInterceptor(
httpNetworkTransport,
subscriptionNetworkTransport
);
}
public ApolloCall query(@NotNull Query operation) {
return new DefaultApolloCall<>(this, operation);
}
public ApolloCall mutation(@NotNull Mutation operation) {
return new DefaultApolloCall<>(this, operation);
}
public ApolloCall subscription(@NotNull Subscription operation) {
return new DefaultApolloCall<>(this, operation);
}
public ApolloDisposable execute(@NotNull ApolloRequest apolloRequest, @NotNull ApolloCallback callback) {
ApolloRequest.Builder requestBuilder = new ApolloRequest.Builder<>(apolloRequest.getOperation())
.addExecutionContext(customScalarAdapters)
.addExecutionContext(apolloRequest.getExecutionContext())
.httpMethod(httpMethod)
.httpHeaders(httpHeaders)
.sendApqExtensions(sendApqExtensions)
.sendDocument(sendDocument)
.enableAutoPersistedQueries(enableAutoPersistedQueries);
if (apolloRequest.getHttpMethod() != null) {
requestBuilder.httpMethod(apolloRequest.getHttpMethod());
}
if (apolloRequest.getHttpHeaders() != null) {
requestBuilder.httpHeaders(apolloRequest.getHttpHeaders());
}
if (apolloRequest.getSendApqExtensions() != null) {
requestBuilder.sendApqExtensions(apolloRequest.getSendApqExtensions());
}
if (apolloRequest.getSendDocument() != null) {
requestBuilder.sendDocument(apolloRequest.getSendDocument());
}
if (apolloRequest.getEnableAutoPersistedQueries() != null) {
requestBuilder.enableAutoPersistedQueries(apolloRequest.getEnableAutoPersistedQueries());
}
if (apolloRequest.getCanBeBatched() != null) {
requestBuilder.addHttpHeader(ExecutionOptions.CAN_BE_BATCHED, apolloRequest.getCanBeBatched().toString());
}
DefaultApolloDisposable disposable = new DefaultApolloDisposable();
ArrayList interceptors = new ArrayList<>(this.interceptors);
interceptors.add(networkInterceptor);
executor.execute(() -> new DefaultInterceptorChain(interceptors, 0, disposable).proceed(requestBuilder.build(), callback));
return disposable;
}
private static class NetworkInterceptor implements ApolloInterceptor {
private NetworkTransport httpNetworkTransport;
private NetworkTransport subscriptionNetworkTransport;
private NetworkInterceptor(NetworkTransport httpNetworkTransport, NetworkTransport subscriptionNetworkTransport) {
this.httpNetworkTransport = httpNetworkTransport;
this.subscriptionNetworkTransport = subscriptionNetworkTransport;
}
@Override
public void intercept(@NotNull ApolloRequest request, @NotNull ApolloInterceptorChain chain, @NotNull ApolloCallback callback) {
if (request.getOperation() instanceof Query || request.getOperation() instanceof Mutation) {
httpNetworkTransport.execute(request, callback, chain.getDisposable());
} else {
subscriptionNetworkTransport.execute(request, callback, chain.getDisposable());
}
}
}
public CustomScalarAdapters getCustomScalarAdapters() {
return customScalarAdapters;
}
public void close() {
if (executor instanceof ApolloDefaultExecutor) {
((ApolloDefaultExecutor) executor).shutdown();
}
}
public static class Builder implements MutableExecutionOptions {
private HttpEngine httpEngine;
private NetworkTransport networkTransport;
private NetworkTransport subscriptionNetworkTransport;
private String httpServerUrl;
private String webSocketServerUrl;
private Call.Factory callFactory;
private WebSocket.Factory webSocketFactory;
private Executor executor;
private List interceptors = new ArrayList<>();
private List httpInterceptors = new ArrayList<>();
private WsProtocol.Factory wsProtocolFactory;
private List wsHeaders = new ArrayList<>();
private WebSocketNetworkTransport.ReopenWhen wsReopenWhen;
private Long wsIdleTimeoutMillis;
private final CustomScalarAdapters.Builder customScalarAdaptersBuilder = new CustomScalarAdapters.Builder();
private ExecutionContext executionContext;
private HttpMethod httpMethod;
private final ArrayList httpHeaders = new ArrayList<>();
private Boolean sendApqExtensions;
private Boolean sendDocument;
private Boolean enableAutoPersistedQueries;
private Boolean canBeBatched;
private Boolean httpExposeErrorBody;
public Builder() {
}
/**
* The url of the GraphQL server used for HTTP. This is the same as {@link #httpServerUrl(String)}. See also
* {@link #networkTransport(NetworkTransport)} for more customization
*/
public Builder serverUrl(@NotNull String serverUrl) {
this.httpServerUrl = checkNotNull(serverUrl, "serverUrl is null");
return this;
}
/**
* The url of the GraphQL server used for HTTP. See also {@link #networkTransport(NetworkTransport)} for more customization
*/
public Builder httpServerUrl(@NotNull String httpServerUrl) {
this.httpServerUrl = checkNotNull(httpServerUrl, "httpServerUrl is null");
return this;
}
/**
* The url of the GraphQL server used for WebSockets. See also {@link #subscriptionNetworkTransport(NetworkTransport)} for more
* customization
*/
public Builder webSocketServerUrl(@NotNull String webSocketServerUrl) {
this.webSocketServerUrl = checkNotNull(webSocketServerUrl, "webSocketServerUrl is null");
return this;
}
/**
* Set the {@link OkHttpClient} to use for making network requests.
*
* @param okHttpClient the client to use.
* @return The {@link Builder} object to be used for chaining method calls
*/
public Builder okHttpClient(@NotNull OkHttpClient okHttpClient) {
this.callFactory = checkNotNull(okHttpClient, "okHttpClient is null");
this.webSocketFactory = okHttpClient;
return null;
}
/**
* Set the custom call factory for creating {@link Call} instances. Note: Calling {@link #okHttpClient(OkHttpClient)} automatically
* sets this value.
*/
public Builder callFactory(@NotNull Call.Factory factory) {
this.callFactory = checkNotNull(factory, "factory is null");
return this;
}
/**
* Set the custom call factory for creating {@link WebSocket} instances.
Note: Calling {@link #okHttpClient(OkHttpClient)}
* automatically sets this value.
*/
public Builder webSocketFactory(@NotNull WebSocket.Factory factory) {
this.webSocketFactory = checkNotNull(factory, "factory is null");
return this;
}
/**
* The {@link Executor} to use for dispatching the requests.
*
* @return The {@link Builder} object to be used for chaining method calls
*/
public Builder dispatcher(@NotNull Executor dispatcher) {
this.executor = checkNotNull(dispatcher, "dispatcher is null");
return this;
}
public Builder addInterceptor(@NotNull ApolloInterceptor interceptor) {
this.interceptors.add(checkNotNull(interceptor, "interceptor is null"));
return this;
}
public Builder addInterceptors(@NotNull List interceptors) {
this.interceptors.addAll(checkNotNull(interceptors, "interceptors is null"));
return this;
}
public Builder interceptors(@NotNull List interceptors) {
this.interceptors = checkNotNull(interceptors, "interceptors is null");
return this;
}
public Builder addHttpInterceptor(@NotNull HttpInterceptor interceptor) {
this.httpInterceptors.add(checkNotNull(interceptor, "interceptor is null"));
return this;
}
public Builder addHttpInterceptors(@NotNull List interceptors) {
this.httpInterceptors.addAll(checkNotNull(interceptors, "interceptors is null"));
return this;
}
public Builder httpInterceptors(@NotNull List interceptors) {
this.httpInterceptors = checkNotNull(interceptors, "interceptors is null");
return this;
}
public Builder customScalarAdapters(@NotNull CustomScalarAdapters customScalarAdapters) {
this.customScalarAdaptersBuilder.clear();
this.customScalarAdaptersBuilder.addAll(customScalarAdapters);
return this;
}
/**
* Registers the given customScalarAdapter.
*
* @param customScalarType a generated {@link CustomScalarType}. Every GraphQL custom scalar has a generated class with a static `type`
* property. For an example, for a `Date` custom scalar, you can use `com.example.Date.type`
* @param customScalarAdapter the {@link Adapter} to use for this custom scalar
*/
public Builder addCustomScalarAdapter(@NotNull CustomScalarType customScalarType, @NotNull Adapter customScalarAdapter) {
customScalarAdaptersBuilder.add(customScalarType, customScalarAdapter);
return this;
}
public Builder wsProtocolFactory(@NotNull WsProtocol.Factory wsProtocolFactory) {
this.wsProtocolFactory = checkNotNull(wsProtocolFactory, "wsProtocolFactory is null");
return this;
}
public Builder addWsHeader(@NotNull HttpHeader header) {
this.wsHeaders.add(checkNotNull(header, "header is null"));
return this;
}
public Builder addWsHeaders(@NotNull List headers) {
this.wsHeaders.addAll(checkNotNull(headers, "headers is null"));
return this;
}
public Builder wsHeaders(@NotNull List headers) {
this.wsHeaders = checkNotNull(headers, "headers is null");
return this;
}
public Builder wsReopenWhen(@NotNull WebSocketNetworkTransport.ReopenWhen reopenWhen) {
this.wsReopenWhen = checkNotNull(reopenWhen, "reopenWhen is null");
return this;
}
public Builder wsIdleTimeoutMillis(long wsIdleTimeoutMillis) {
this.wsIdleTimeoutMillis = wsIdleTimeoutMillis;
return this;
}
public Builder httpEngine(@NotNull HttpEngine httpEngine) {
this.httpEngine = checkNotNull(httpEngine, "httpEngine is null");
return this;
}
public Builder httpExposeErrorBody(boolean httpExposeErrorBody) {
this.httpExposeErrorBody = httpExposeErrorBody;
return this;
}
public Builder networkTransport(@NotNull NetworkTransport networkTransport) {
this.networkTransport = checkNotNull(networkTransport, "networkTransport is null");
return this;
}
public Builder subscriptionNetworkTransport(@NotNull NetworkTransport subscriptionNetworkTransport) {
this.subscriptionNetworkTransport = checkNotNull(subscriptionNetworkTransport, "subscriptionNetworkTransport is null");
return this;
}
public ApolloClient build() {
if (executor == null) {
executor = defaultExecutor();
}
NetworkTransport networkTransport;
if (this.networkTransport != null) {
if (httpServerUrl != null) throw new IllegalStateException("Apollo: 'httpServerUrl' has no effect if 'networkTransport' is set");
if (httpEngine != null) throw new IllegalStateException("Apollo: 'httpEngine' has no effect if 'networkTransport' is set");
if (callFactory != null) throw new IllegalStateException("Apollo: 'callFactory' has no effect if 'networkTransport' is set");
if (httpExposeErrorBody != null)
throw new IllegalStateException("Apollo: 'httpExposeErrorBody' has no effect if 'networkTransport' is set");
networkTransport = this.networkTransport;
} else {
checkNotNull(httpServerUrl, "serverUrl is missing");
if (callFactory != null) {
if (httpEngine != null) {
throw new IllegalStateException("Apollo: 'httpEngine' has no effect if 'callFactory' is set");
}
} else {
callFactory = new OkHttpClient();
}
if (httpEngine == null) {
httpEngine = new OkHttpHttpEngine(callFactory);
}
if (httpExposeErrorBody == null) {
httpExposeErrorBody = false;
}
networkTransport = new HttpNetworkTransport(new DefaultHttpRequestComposer(httpServerUrl), httpEngine, httpInterceptors, httpExposeErrorBody);
}
NetworkTransport subscriptionNetworkTransport;
if (this.subscriptionNetworkTransport != null) {
if (webSocketServerUrl != null)
throw new IllegalStateException("Apollo: 'webSocketServerUrl' has no effect if 'subscriptionNetworkTransport' is set");
if (webSocketFactory != null)
throw new IllegalStateException("Apollo: 'webSocketFactory' has no effect if 'subscriptionNetworkTransport' is set");
if (wsProtocolFactory != null)
throw new IllegalStateException("Apollo: 'wsProtocolFactory' has no effect if 'subscriptionNetworkTransport' is set");
if (wsHeaders != null)
throw new IllegalStateException("Apollo: 'wsHeaders' has no effect if 'subscriptionNetworkTransport' is set");
if (wsIdleTimeoutMillis != null)
throw new IllegalStateException("Apollo: 'wsIdleTimeoutMillis' has no effect if 'subscriptionNetworkTransport' is set");
subscriptionNetworkTransport = this.subscriptionNetworkTransport;
} else {
if (webSocketServerUrl == null) {
webSocketServerUrl = httpServerUrl;
}
if (webSocketServerUrl == null) {
// Fallback to the regular NetworkTransport. This is unlikely to work but chances are
// that the user is not going to use subscription, so it's better than failing
subscriptionNetworkTransport = networkTransport;
} else {
if (webSocketFactory == null) {
webSocketFactory = callFactory instanceof OkHttpClient ? (OkHttpClient) callFactory : new OkHttpClient();
}
if (wsProtocolFactory == null) {
wsProtocolFactory = new GraphQLWsProtocol.Factory();
}
if (wsReopenWhen == null) {
wsReopenWhen = (throwable, attempt) -> false;
}
if (wsIdleTimeoutMillis == null) {
wsIdleTimeoutMillis = 60_000L;
}
subscriptionNetworkTransport = new WebSocketNetworkTransport(
webSocketFactory,
wsProtocolFactory,
webSocketServerUrl,
wsHeaders,
wsReopenWhen,
executor,
wsIdleTimeoutMillis
);
}
}
return new ApolloClient(
executor,
networkTransport,
subscriptionNetworkTransport,
interceptors,
customScalarAdaptersBuilder.build(),
httpMethod,
httpHeaders,
sendApqExtensions,
sendDocument,
enableAutoPersistedQueries,
canBeBatched
);
}
public Builder autoPersistedQueries() {
return autoPersistedQueries(HttpMethod.Get, HttpMethod.Post, true);
}
public Builder autoPersistedQueries(
HttpMethod httpMethodForHashedQueries
) {
return autoPersistedQueries(httpMethodForHashedQueries, HttpMethod.Post, true);
}
public Builder autoPersistedQueries(
HttpMethod httpMethodForHashedQueries,
HttpMethod httpMethodForDocumentQueries
) {
return autoPersistedQueries(httpMethodForHashedQueries, httpMethodForDocumentQueries, true);
}
public Builder autoPersistedQueries(
HttpMethod httpMethodForHashedQueries,
HttpMethod httpMethodForDocumentQueries,
boolean enableByDefault
) {
addInterceptor(
new AutoPersistedQueryInterceptor(
httpMethodForHashedQueries,
httpMethodForDocumentQueries
)
);
enableAutoPersistedQueries(enableByDefault);
return this;
}
/**
* Batch HTTP queries to execute multiple at once. This reduces the number of HTTP round trips at the price of increased latency as
* every request in the batch is now as slow as the slowest one. Some servers might have a per-HTTP-call cache making it faster to
* resolve 1 big array of n queries compared to resolving the n queries separately.
*
* See also {@link BatchingHttpInterceptor}.
*
* @param batchIntervalMillis the interval between two batches
* @param maxBatchSize always send the batch when this threshold is reached
*/
public Builder httpBatching(long batchIntervalMillis, int maxBatchSize, boolean enableByDefault) {
addHttpInterceptor(new BatchingHttpInterceptor(batchIntervalMillis, maxBatchSize, httpExposeErrorBody != null && httpExposeErrorBody));
canBeBatched(enableByDefault);
return this;
}
@NotNull @Override public ExecutionContext getExecutionContext() {
return executionContext;
}
@Nullable @Override public HttpMethod getHttpMethod() {
return httpMethod;
}
@Nullable @Override public List getHttpHeaders() {
return httpHeaders;
}
@Nullable @Override public Boolean getSendApqExtensions() {
return sendApqExtensions;
}
@Nullable @Override public Boolean getSendDocument() {
return sendDocument;
}
@Nullable @Override public Boolean getEnableAutoPersistedQueries() {
return enableAutoPersistedQueries;
}
@Nullable @Override public Boolean getCanBeBatched() {
return canBeBatched;
}
@Override public Builder addExecutionContext(@NotNull ExecutionContext executionContext) {
this.executionContext = this.executionContext.plus(executionContext);
return this;
}
@Override public Builder httpMethod(@Nullable HttpMethod httpMethod) {
this.httpMethod = httpMethod;
return this;
}
@Override public Builder httpHeaders(@Nullable List list) {
this.httpHeaders.clear();
this.httpHeaders.addAll(list);
return this;
}
@Override public Builder addHttpHeader(@NotNull String name, @NotNull String value) {
this.httpHeaders.add(new HttpHeader(name, value));
return this;
}
@Override public Builder sendApqExtensions(@Nullable Boolean sendApqExtensions) {
this.sendApqExtensions = sendApqExtensions;
return this;
}
@Override public Builder sendDocument(@Nullable Boolean sendDocument) {
this.sendDocument = sendDocument;
return this;
}
@Override public Builder enableAutoPersistedQueries(@Nullable Boolean enableAutoPersistedQueries) {
this.enableAutoPersistedQueries = enableAutoPersistedQueries;
return this;
}
@Override public Builder canBeBatched(@Nullable Boolean canBeBatched) {
this.canBeBatched = canBeBatched;
return this;
}
}
private static class ApolloDefaultExecutor implements Executor {
private final ExecutorService executor = newCachedThreadPool(runnable -> new Thread(runnable, "Apollo Dispatcher"));
@Override public void execute(@NotNull Runnable command) {
executor.execute(command);
}
public void shutdown() {
executor.shutdown();
}
}
static private Executor defaultExecutor() {
return new ApolloDefaultExecutor();
}
}