package com.apollographql.apollo; import com.apollographql.apollo.api.Mutation; import com.apollographql.apollo.api.Operation; import com.apollographql.apollo.api.OperationName; import com.apollographql.apollo.api.Query; import com.apollographql.apollo.api.ResponseFieldMapper; import com.apollographql.apollo.api.ScalarType; import com.apollographql.apollo.api.internal.Optional; import com.apollographql.apollo.cache.CacheHeaders; import com.apollographql.apollo.cache.http.HttpCachePolicy; import com.apollographql.apollo.cache.http.HttpCacheStore; import com.apollographql.apollo.cache.normalized.ApolloStore; import com.apollographql.apollo.cache.normalized.CacheControl; import com.apollographql.apollo.cache.normalized.CacheKeyResolver; import com.apollographql.apollo.cache.normalized.NormalizedCache; import com.apollographql.apollo.cache.normalized.NormalizedCacheFactory; import com.apollographql.apollo.cache.normalized.RecordFieldAdapter; import com.apollographql.apollo.interceptor.ApolloInterceptor; import com.apollographql.apollo.internal.RealApolloCall; import com.apollographql.apollo.internal.RealApolloPrefetch; import com.apollographql.apollo.internal.cache.http.HttpCache; import com.apollographql.apollo.internal.cache.normalized.RealApolloStore; import com.apollographql.apollo.internal.util.ApolloLogger; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import javax.annotation.Nullable; import okhttp3.Call; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Response; import static com.apollographql.apollo.api.internal.Utils.checkNotNull; /** * ApolloClient class represents the abstraction for the graphQL client that will be used to execute queries and read * the responses back. * * <h3>ApolloClient should be shared</h3> * * Since each ApolloClient holds its own connection pool and thread pool, it is recommended to only create a single * ApolloClient and use that for execution of all the queries, as this would reduce latency and would also save memory. * Conversely, creating a client for each query execution would result in resource wastage on idle pools. * * * <p>See the {@link ApolloClient.Builder} class for configuring the ApolloClient. */ public final class ApolloClient implements ApolloQueryCall.Factory, ApolloMutationCall.Factory, ApolloPrefetch.Factory { public static Builder builder() { return new Builder(); } private final HttpUrl serverUrl; private final Call.Factory httpCallFactory; private final HttpCache httpCache; private final ApolloStore apolloStore; private final Map<ScalarType, CustomTypeAdapter> customTypeAdapters; private final Moshi moshi; private final Map<Class, ResponseFieldMapper> responseFieldMapperPool = new LinkedHashMap<>(); private final ExecutorService dispatcher; private final HttpCachePolicy.Policy defaultHttpCachePolicy; private final CacheControl defaultCacheControl; private final CacheHeaders defaultCacheHeaders; private final ApolloLogger logger; private final List<ApolloInterceptor> applicationInterceptors; private ApolloClient(Builder builder) { this.serverUrl = builder.serverUrl; this.httpCallFactory = builder.okHttpClient; this.httpCache = builder.httpCache; this.apolloStore = builder.apolloStore; this.customTypeAdapters = builder.customTypeAdapters; this.moshi = builder.moshi; this.dispatcher = builder.dispatcher; this.defaultHttpCachePolicy = builder.defaultHttpCachePolicy; this.defaultCacheHeaders = builder.defaultCacheHeaders; this.defaultCacheControl = builder.defaultCacheControl; this.logger = builder.apolloLogger; this.applicationInterceptors = builder.applicationInterceptors; } @Override public <D extends Mutation.Data, T, V extends Mutation.Variables> ApolloMutationCall<T> mutate( @Nonnull Mutation<D, T, V> mutation) { return newCall(mutation); } @Override public <D extends Query.Data, T, V extends Query.Variables> ApolloQueryCall<T> query(@Nonnull Query<D, T, V> query) { return newCall(query); } /** * Prepares the {@link ApolloPrefetch} which will be executed at some point in the future. */ @Override public <D extends Operation.Data, T, V extends Operation.Variables> ApolloPrefetch prefetch( @Nonnull Operation<D, T, V> operation) { return new RealApolloPrefetch(operation, serverUrl, httpCallFactory, httpCache, moshi, dispatcher, logger); } /** * @return The default {@link CacheHeaders} which this instance of {@link ApolloClient} was configured. */ public CacheHeaders defaultCacheHeaders() { return defaultCacheHeaders; } void clearHttpCache() { if (httpCache != null) { httpCache.clear(); } } /** * Clear all entries from the normalized cache. */ public void clearNormalizedCache() { apolloStore.clearAll(); } /** * @return The {@link ApolloStore} managing access to the normalized cache created by {@link * Builder#normalizedCache(NormalizedCacheFactory, CacheKeyResolver)} } */ public ApolloStore apolloStore() { return apolloStore; } Response cachedHttpResponse(String cacheKey) throws IOException { if (httpCache != null) { return httpCache.read(cacheKey); } else { return null; } } private <D extends Operation.Data, T, V extends Operation.Variables> RealApolloCall<T> newCall( @Nonnull Operation<D, T, V> operation) { ResponseFieldMapper responseFieldMapper = responseFieldMapper(operation); return RealApolloCall.<T>builder() .operation(operation) .serverUrl(serverUrl) .httpCallFactory(httpCallFactory) .httpCache(httpCache) .httpCachePolicy(defaultHttpCachePolicy) .moshi(moshi) .responseFieldMapper(responseFieldMapper) .customTypeAdapters(customTypeAdapters) .apolloStore(apolloStore) .cacheControl(defaultCacheControl) .cacheHeaders(defaultCacheHeaders) .dispatcher(dispatcher) .logger(logger) .applicationInterceptors(applicationInterceptors) .refetchQueryNames(Collections.<OperationName>emptyList()) .build(); } private ResponseFieldMapper responseFieldMapper(Operation operation) { Optional<ResponseFieldMapper> responseFieldMapper; synchronized (responseFieldMapperPool) { responseFieldMapper = Optional.fromNullable(responseFieldMapperPool.get(operation.getClass())); if (!responseFieldMapper.isPresent()) { responseFieldMapper = Optional.of(operation.responseFieldMapper()); responseFieldMapperPool.put(operation.getClass(), responseFieldMapper.get()); } } return responseFieldMapper.get(); } @SuppressWarnings("WeakerAccess") public static class Builder { OkHttpClient okHttpClient; HttpUrl serverUrl; HttpCacheStore httpCacheStore; ApolloStore apolloStore = ApolloStore.NO_APOLLO_STORE; Optional<NormalizedCacheFactory> cacheFactory = Optional.absent(); Optional<CacheKeyResolver> cacheKeyResolver = Optional.absent(); HttpCachePolicy.Policy defaultHttpCachePolicy = HttpCachePolicy.NETWORK_ONLY; CacheControl defaultCacheControl = CacheControl.CACHE_FIRST; CacheHeaders defaultCacheHeaders = CacheHeaders.NONE; final Map<ScalarType, CustomTypeAdapter> customTypeAdapters = new LinkedHashMap<>(); private final Moshi.Builder moshiBuilder = new Moshi.Builder(); Moshi moshi; ExecutorService dispatcher; Optional<Logger> logger = Optional.absent(); HttpCache httpCache; ApolloLogger apolloLogger; final List<ApolloInterceptor> applicationInterceptors = new ArrayList<>(); private Builder() { } /** * 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(@Nonnull OkHttpClient okHttpClient) { this.okHttpClient = checkNotNull(okHttpClient, "okHttpClient is null"); return this; } /** * <p>Set the API server's base url.</p> * * @param serverUrl the url to set. * @return The {@link Builder} object to be used for chaining method calls */ public Builder serverUrl(@Nonnull HttpUrl serverUrl) { this.serverUrl = checkNotNull(serverUrl, "serverUrl is null"); return this; } /** * <p>Set the API server's base url.</p> * * @param serverUrl the url to set. * @return The {@link Builder} object to be used for chaining method calls */ public Builder serverUrl(@Nonnull String serverUrl) { this.serverUrl = HttpUrl.parse(checkNotNull(serverUrl, "serverUrl == null")); return this; } /** * Set the configuration to be used for request/response http cache. * * @param cacheStore The store to use for reading and writing cached response. * @return The {@link Builder} object to be used for chaining method calls */ public Builder httpCacheStore(@Nonnull HttpCacheStore cacheStore) { this.httpCacheStore = checkNotNull(cacheStore, "cacheStore == null"); return this; } /** * Set the configuration to be used for normalized cache. * * @param normalizedCacheFactory the {@link NormalizedCacheFactory} used to construct a {@link NormalizedCache}. * @return The {@link Builder} object to be used for chaining method calls */ public Builder normalizedCache(@Nonnull NormalizedCacheFactory normalizedCacheFactory) { return normalizedCache(normalizedCacheFactory, CacheKeyResolver.DEFAULT); } /** * Set the configuration to be used for normalized cache. * * @param normalizedCacheFactory the {@link NormalizedCacheFactory} used to construct a {@link NormalizedCache}. * @param keyResolver the {@link CacheKeyResolver} to use to normalize records * @return The {@link Builder} object to be used for chaining method calls */ public Builder normalizedCache(@Nonnull NormalizedCacheFactory normalizedCacheFactory, @Nonnull CacheKeyResolver keyResolver) { cacheFactory = Optional.fromNullable(checkNotNull(normalizedCacheFactory, "normalizedCacheFactory == null")); cacheKeyResolver = Optional.fromNullable(checkNotNull(keyResolver, "cacheKeyResolver == null")); return this; } /** * Set the type adapter to use for serializing and de-serializing custom GraphQL scalar types. * * @param scalarType the scalar type to serialize/deserialize * @param customTypeAdapter the type adapter to use * @param <T> the value type * @return The {@link Builder} object to be used for chaining method calls */ public <T> Builder addCustomTypeAdapter(@Nonnull ScalarType scalarType, @Nonnull final CustomTypeAdapter<T> customTypeAdapter) { customTypeAdapters.put(scalarType, customTypeAdapter); moshiBuilder.add(scalarType.javaType(), new JsonAdapter<T>() { @Override public T fromJson(com.squareup.moshi.JsonReader reader) throws IOException { return customTypeAdapter.decode(reader.nextString()); } @Override public void toJson(JsonWriter writer, T value) throws IOException { //noinspection unchecked writer.value(customTypeAdapter.encode(value)); } }); return this; } /** * The #{@link ExecutorService} to use for dispatching the requests. * * @return The {@link Builder} object to be used for chaining method calls */ public Builder dispatcher(@Nonnull ExecutorService dispatcher) { this.dispatcher = checkNotNull(dispatcher, "dispatcher == null"); return this; } /** * Sets the http cache policy to be used as default for all GraphQL {@link Query} operations. Will be ignored for * any {@link Mutation} operations. By default http cache policy is set to {@link HttpCachePolicy#NETWORK_ONLY}. * * @return The {@link Builder} object to be used for chaining method calls */ public Builder defaultHttpCachePolicy(@Nonnull HttpCachePolicy.Policy cachePolicy) { this.defaultHttpCachePolicy = checkNotNull(cachePolicy, "cachePolicy == null"); return this; } /** * Set the default {@link CacheControl} strategy that will be used for each new * {@link ApolloCall}. * * @return The {@link Builder} object to be used for chaining method calls */ public Builder defaultCacheControl(@Nonnull CacheControl cacheControl) { this.defaultCacheControl = checkNotNull(cacheControl, "cacheControl == null"); return this; } /** * Set the default {@link CacheHeaders} strategy that will be passed * to each new {@link ApolloCall}. * * @return The {@link Builder} object to be used for chaining method calls */ public Builder defaultCacheHeaders(@Nonnull CacheHeaders cacheHeaders) { this.defaultCacheHeaders = checkNotNull(cacheHeaders, "cacheHeaders == null"); return this; } /** * The {@link Logger} to use for logging purposes. * * @return The {@link Builder} object to be used for chaining method calls */ public Builder logger(@Nullable Logger logger) { this.logger = Optional.fromNullable(logger); return this; } /** * <p>Adds an interceptor that observes the full span of each call: from before the connection is established until * after the response source is selected (either the server, cache or both). This method can be called multiple * times for adding multiple application interceptors. </p> * * <p>Note: Interceptors will be called <b>in the order in which they are added to the list of interceptors</b> * and if any of the interceptors tries to short circuit the responses, then subsequent interceptors <b>won't</b> be * called.</p> * * @param interceptor Application level interceptor to add * @return The {@link Builder} object to be used for chaining method calls */ public Builder addApplicationInterceptor(@Nonnull ApolloInterceptor interceptor) { applicationInterceptors.add(interceptor); return this; } /** * Builds the {@link ApolloClient} instance using the configured values. * * Note that if the {@link #dispatcher} is not called, then a default {@link ExecutorService} is used. * * @return The configured {@link ApolloClient} */ public ApolloClient build() { checkNotNull(okHttpClient, "okHttpClient is null"); checkNotNull(serverUrl, "serverUrl is null"); apolloLogger = new ApolloLogger(logger); moshi = moshiBuilder.build(); if (httpCacheStore != null) { httpCache = new HttpCache(httpCacheStore, apolloLogger); okHttpClient = okHttpClient.newBuilder().addInterceptor(httpCache.interceptor()).build(); } if (cacheFactory.isPresent() && cacheKeyResolver.isPresent()) { final NormalizedCache normalizedCache = cacheFactory.get().createNormalizedCache(RecordFieldAdapter.create(moshi)); this.apolloStore = new RealApolloStore(normalizedCache, cacheKeyResolver.get(), customTypeAdapters, apolloLogger); } if (dispatcher == null) { dispatcher = defaultDispatcher(); } return new ApolloClient(this); } private ExecutorService defaultDispatcher() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() { @Override public Thread newThread(Runnable runnable) { return new Thread(runnable, "Apollo Dispatcher"); } }); } } }