package com.apollographql.apollo.internal.interceptor;
import com.apollographql.apollo.api.Operation;
import com.apollographql.apollo.api.internal.Optional;
import com.apollographql.apollo.cache.http.HttpCachePolicy;
import com.apollographql.apollo.exception.ApolloException;
import com.apollographql.apollo.exception.ApolloNetworkException;
import com.apollographql.apollo.interceptor.ApolloInterceptor;
import com.apollographql.apollo.interceptor.ApolloInterceptorChain;
import com.apollographql.apollo.internal.cache.http.HttpCache;
import com.apollographql.apollo.internal.util.ApolloLogger;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.Buffer;
import static com.apollographql.apollo.api.internal.Utils.checkNotNull;
/**
* ApolloServerInterceptor is a concrete {@link ApolloInterceptor} responsible for making the network calls to the
* server. It is the last interceptor in the chain of interceptors and hence doesn't call
* {@link ApolloInterceptorChain#proceed()} on the interceptor chain.
*/
@SuppressWarnings("WeakerAccess") public final class ApolloServerInterceptor implements ApolloInterceptor {
private static final String ACCEPT_TYPE = "application/json";
private static final String CONTENT_TYPE = "application/json";
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
final HttpUrl serverUrl;
final okhttp3.Call.Factory httpCallFactory;
final Optional<HttpCachePolicy.Policy> cachePolicy;
final boolean prefetch;
final Moshi moshi;
final ApolloLogger logger;
volatile Call httpCall;
public ApolloServerInterceptor(@Nonnull HttpUrl serverUrl, @Nonnull Call.Factory httpCallFactory,
@Nullable HttpCachePolicy.Policy cachePolicy, boolean prefetch, @Nonnull Moshi moshi,
@Nonnull ApolloLogger logger) {
this.serverUrl = checkNotNull(serverUrl, "serverUrl == null");
this.httpCallFactory = checkNotNull(httpCallFactory, "httpCallFactory == null");
this.cachePolicy = Optional.fromNullable(cachePolicy);
this.prefetch = prefetch;
this.moshi = checkNotNull(moshi, "moshi == null");
this.logger = checkNotNull(logger, "logger == null");
}
@Override @Nonnull public InterceptorResponse intercept(Operation operation, ApolloInterceptorChain chain)
throws ApolloException {
try {
httpCall = httpCall(operation);
} catch (IOException e) {
logger.e(e, "Failed to prepare http call");
throw new ApolloNetworkException("Failed to prepare http call", e);
}
try {
return new InterceptorResponse(httpCall.execute());
} catch (IOException e) {
logger.e(e, "Failed to execute http call");
throw new ApolloNetworkException("Failed to execute http call", e);
}
}
@Override
public void interceptAsync(@Nonnull final Operation operation, @Nonnull final ApolloInterceptorChain chain,
@Nonnull ExecutorService dispatcher, @Nonnull final CallBack callBack) {
dispatcher.execute(new Runnable() {
@Override public void run() {
try {
httpCall = httpCall(operation);
} catch (IOException e) {
logger.e(e, "Failed to prepare http call");
callBack.onFailure(new ApolloNetworkException("Failed to prepare http call", e));
return;
}
httpCall.enqueue(new Callback() {
@Override public void onFailure(Call call, IOException e) {
logger.e(e, "Failed to execute http call");
callBack.onFailure(new ApolloNetworkException("Failed to execute http call", e));
}
@Override public void onResponse(Call call, Response response) throws IOException {
callBack.onResponse(new ApolloInterceptor.InterceptorResponse(response));
}
});
}
});
}
@Override public void dispose() {
Call httpCall = this.httpCall;
if (httpCall != null) {
httpCall.cancel();
}
this.httpCall = null;
}
private Call httpCall(Operation operation) throws IOException {
RequestBody requestBody = httpRequestBody(operation);
Request.Builder requestBuilder = new Request.Builder()
.url(serverUrl)
.post(requestBody)
.header("Accept", ACCEPT_TYPE)
.header("Content-Type", CONTENT_TYPE);
if (cachePolicy.isPresent()) {
HttpCachePolicy.Policy cachePolicy = this.cachePolicy.get();
String cacheKey = cacheKey(requestBody);
requestBuilder = requestBuilder
.header(HttpCache.CACHE_KEY_HEADER, cacheKey)
.header(HttpCache.CACHE_FETCH_STRATEGY_HEADER, cachePolicy.fetchStrategy.name())
.header(HttpCache.CACHE_EXPIRE_TIMEOUT_HEADER, String.valueOf(cachePolicy.expireTimeoutMs()))
.header(HttpCache.CACHE_EXPIRE_AFTER_READ_HEADER, Boolean.toString(cachePolicy.expireAfterRead))
.header(HttpCache.CACHE_PREFETCH_HEADER, Boolean.toString(prefetch));
}
return httpCallFactory.newCall(requestBuilder.build());
}
private RequestBody httpRequestBody(Operation operation) throws IOException {
JsonAdapter<Operation> adapter = new OperationJsonAdapter(moshi);
Buffer buffer = new Buffer();
adapter.toJson(buffer, operation);
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
}
public static String cacheKey(RequestBody requestBody) throws IOException {
Buffer hashBuffer = new Buffer();
requestBody.writeTo(hashBuffer);
return hashBuffer.readByteString().md5().hex();
}
static final class OperationJsonAdapter extends JsonAdapter<Operation> {
private final Moshi moshi;
OperationJsonAdapter(Moshi moshi) {
this.moshi = moshi;
}
@Override public Operation fromJson(JsonReader reader) throws IOException {
throw new IllegalStateException("This should not be called ever.");
}
@Override public void toJson(JsonWriter writer, Operation value) throws IOException {
writer.beginObject();
writer.name("query").value(value.queryDocument().replaceAll("\\n", ""));
Operation.Variables variables = value.variables();
if (variables != null) {
//noinspection unchecked
JsonAdapter<Operation.Variables> adapter =
(JsonAdapter<Operation.Variables>) moshi.adapter(variables.getClass());
writer.name("variables");
adapter.toJson(writer, variables);
}
writer.endObject();
}
}
}