package com.apollographql.apollo.internal.interceptor;
import com.apollographql.apollo.CustomTypeAdapter;
import com.apollographql.apollo.api.Operation;
import com.apollographql.apollo.api.Response;
import com.apollographql.apollo.api.ResponseFieldMapper;
import com.apollographql.apollo.api.ScalarType;
import com.apollographql.apollo.cache.CacheHeaders;
import com.apollographql.apollo.cache.normalized.ApolloStore;
import com.apollographql.apollo.cache.normalized.CacheControl;
import com.apollographql.apollo.cache.normalized.Record;
import com.apollographql.apollo.exception.ApolloException;
import com.apollographql.apollo.interceptor.ApolloInterceptor;
import com.apollographql.apollo.interceptor.ApolloInterceptorChain;
import com.apollographql.apollo.internal.cache.normalized.ResponseNormalizer;
import com.apollographql.apollo.internal.cache.normalized.Transaction;
import com.apollographql.apollo.internal.cache.normalized.WriteableStore;
import com.apollographql.apollo.internal.util.ApolloLogger;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static com.apollographql.apollo.api.internal.Utils.checkNotNull;
/**
* ApolloCacheInterceptor is a concrete {@link ApolloInterceptor} responsible for serving requests from the normalized
* cache. It takes the following actions based on the {@link CacheControl} set:
*
* <ol> <li> <b>CACHE_ONLY</b>: First tries to get the data from the normalized cache. If the data doesn't exist or
* there was an error inflating the models, it returns the
* {@link com.apollographql.apollo.interceptor.ApolloInterceptor.InterceptorResponse}
* with the GraphQL {@link Operation} object wrapped inside. </li>
*
* <li><b>CACHE_FIRST</b>: First tries to get the data from the normalized cache. If the data doesn't exist or there was
* an error inflating the models, it then makes a network request.</li>
*
* <li><b>NETWORK_FIRST</b>: First tries to get the data from the network. If there was an error getting data from the
* network, it tries to get it from the normalized cache. If it is not present in the cache, then it rethrows the
* network exception.</li>
*
* <li><b>NETWORK_ONLY</b>: First tries to get the data from the network. If the network request fails, it throws an
* exception.</li>
*
* </ol>
*/
public final class ApolloCacheInterceptor implements ApolloInterceptor {
private final ApolloStore apolloStore;
private final CacheControl cacheControl;
private final CacheHeaders cacheHeaders;
private final ResponseFieldMapper responseFieldMapper;
private final Map<ScalarType, CustomTypeAdapter> customTypeAdapters;
private final ExecutorService dispatcher;
private final ApolloLogger logger;
public ApolloCacheInterceptor(@Nonnull ApolloStore apolloStore, @Nonnull CacheControl cacheControl,
@Nonnull CacheHeaders cacheHeaders,
@Nonnull ResponseFieldMapper responseFieldMapper,
@Nonnull Map<ScalarType, CustomTypeAdapter> customTypeAdapters,
@Nonnull ExecutorService dispatcher, @Nonnull ApolloLogger logger) {
this.apolloStore = checkNotNull(apolloStore, "cache == null");
this.cacheControl = checkNotNull(cacheControl, "cacheControl == null");
this.cacheHeaders = checkNotNull(cacheHeaders, "cacheHeaders == null");
this.responseFieldMapper = checkNotNull(responseFieldMapper, "responseFieldMapper == null");
this.customTypeAdapters = checkNotNull(customTypeAdapters, "customTypeAdapters == null");
this.dispatcher = checkNotNull(dispatcher, "dispatcher == null");
this.logger = checkNotNull(logger, "logger == null");
}
@Nonnull @Override public InterceptorResponse intercept(Operation operation, ApolloInterceptorChain chain)
throws ApolloException {
InterceptorResponse cachedResponse = resolveCacheFirstResponse(operation);
if (cachedResponse != null) {
return cachedResponse;
}
InterceptorResponse networkResponse;
try {
networkResponse = chain.proceed();
} catch (Exception e) {
InterceptorResponse networkFirstCacheResponse = resolveNetworkFirstCacheResponse(operation);
if (networkFirstCacheResponse != null) {
logger.d(e, "Failed to fetch network response for operation %s, return cached one", operation);
return networkFirstCacheResponse;
}
throw e;
}
return handleNetworkResponse(operation, networkResponse);
}
@Override
public void interceptAsync(@Nonnull final Operation operation, @Nonnull final ApolloInterceptorChain chain,
@Nonnull final ExecutorService dispatcher, @Nonnull final CallBack callBack) {
dispatcher.execute(new Runnable() {
@Override public void run() {
//Imperative strategy
final InterceptorResponse cachedResponse = resolveCacheFirstResponse(operation);
if (cachedResponse != null) {
callBack.onResponse(cachedResponse);
return;
}
chain.proceedAsync(dispatcher, new CallBack() {
@Override public void onResponse(@Nonnull InterceptorResponse response) {
callBack.onResponse(handleNetworkResponse(operation, response));
}
@Override public void onFailure(@Nonnull ApolloException e) {
InterceptorResponse response = resolveNetworkFirstCacheResponse(operation);
if (response != null) {
logger.d(e, "Failed to fetch network response for operation %s, return cached one", operation);
callBack.onResponse(response);
} else {
callBack.onFailure(e);
}
}
});
}
});
}
@Override public void dispose() {
//no op
}
private InterceptorResponse resolveCacheFirstResponse(Operation operation) {
if (cacheControl == CacheControl.CACHE_ONLY || cacheControl == CacheControl.CACHE_FIRST) {
ResponseNormalizer<Record> responseNormalizer = apolloStore.cacheResponseNormalizer();
Response cachedResponse = apolloStore.read(operation, responseFieldMapper, responseNormalizer, cacheHeaders);
if (cacheControl == CacheControl.CACHE_ONLY || cachedResponse.data() != null) {
logger.d("Cache HIT for operation %s", operation);
return new InterceptorResponse(null, cachedResponse, responseNormalizer.records());
}
}
logger.d("Cache MISS for operation %s", operation);
return null;
}
private InterceptorResponse handleNetworkResponse(Operation operation, InterceptorResponse networkResponse) {
boolean networkFailed = (!networkResponse.httpResponse.isPresent()
|| !networkResponse.httpResponse.get().isSuccessful());
if (networkFailed && cacheControl != CacheControl.NETWORK_ONLY) {
ResponseNormalizer<Record> responseNormalizer = apolloStore.cacheResponseNormalizer();
Response cachedResponse = apolloStore.read(operation, responseFieldMapper, responseNormalizer, cacheHeaders);
if (cachedResponse.data() != null) {
return new InterceptorResponse(networkResponse.httpResponse.get(), cachedResponse,
responseNormalizer.records());
}
}
final Collection<Record> records = networkResponse.cacheRecords.orNull();
if (records != null) {
dispatcher.execute(new Runnable() {
@Override public void run() {
Set<String> changedKeys = apolloStore.writeTransaction(new Transaction<WriteableStore, Set<String>>() {
@Nullable @Override public Set<String> execute(WriteableStore cache) {
return cache.merge(records, cacheHeaders);
}
});
apolloStore.publish(changedKeys);
}
});
}
return networkResponse;
}
private InterceptorResponse resolveNetworkFirstCacheResponse(Operation operation) {
if (cacheControl == CacheControl.NETWORK_FIRST) {
ResponseNormalizer<Record> responseNormalizer = apolloStore.cacheResponseNormalizer();
Response cachedResponse = apolloStore.read(operation, responseFieldMapper, responseNormalizer, cacheHeaders);
if (cachedResponse.data() != null) {
logger.d("Cache HIT for operation %s", operation);
return new InterceptorResponse(null, cachedResponse, responseNormalizer.records());
}
}
logger.d("Cache MISS for operation %s", operation);
return null;
}
}