package com.apollographql.apollo.internal.cache.http; import com.apollographql.apollo.internal.util.ApolloLogger; import com.apollographql.apollo.cache.http.HttpCacheRecordEditor; import java.io.IOException; import javax.annotation.Nonnull; import okhttp3.MediaType; import okhttp3.Response; import okhttp3.ResponseBody; import okhttp3.internal.http.HttpCodec; import okio.Buffer; import okio.BufferedSource; import okio.Okio; import okio.Source; import okio.Timeout; import static com.apollographql.apollo.api.internal.Utils.checkNotNull; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static okhttp3.internal.Util.closeQuietly; import static okhttp3.internal.Util.discard; final class ResponseBodyProxy extends ResponseBody { private final String contentType; private final String contentLength; private final Source responseBodySource; ResponseBodyProxy(@Nonnull HttpCacheRecordEditor cacheRecordEditor, @Nonnull Response sourceResponse, @Nonnull ApolloLogger logger) { checkNotNull(cacheRecordEditor, "cacheRecordEditor == null"); checkNotNull(sourceResponse, "sourceResponse == null"); checkNotNull(logger, "logger == null"); this.contentType = sourceResponse.header("Content-Type"); this.contentLength = sourceResponse.header("Content-Length"); this.responseBodySource = new ProxySource(cacheRecordEditor, sourceResponse.body().source(), logger); } @Override public MediaType contentType() { return contentType != null ? MediaType.parse(contentType) : null; } @Override public long contentLength() { try { return contentLength != null ? Long.parseLong(contentLength) : -1; } catch (NumberFormatException e) { return -1; } } @Override public BufferedSource source() { return Okio.buffer(responseBodySource); } private static class ProxySource implements Source { final HttpCacheRecordEditor cacheRecordEditor; final ResponseBodyCacheSink responseBodyCacheSink; final Source responseBodySource; final ApolloLogger logger; boolean closed; ProxySource(HttpCacheRecordEditor cacheRecordEditor, Source responseBodySource, final ApolloLogger logger) { this.cacheRecordEditor = cacheRecordEditor; this.responseBodySource = responseBodySource; this.logger = logger; responseBodyCacheSink = new ResponseBodyCacheSink(Okio.buffer(cacheRecordEditor.bodySink())) { @Override void onException(Exception e) { abortCacheQuietly(); logger.w(e, "Operation failed"); } }; } @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead; try { bytesRead = responseBodySource.read(sink, byteCount); } catch (IOException e) { if (!closed) { // Failed to write a complete cache response. closed = true; abortCacheQuietly(); } throw e; } if (bytesRead == -1) { if (!closed) { // The cache response is complete! closed = true; commitCache(); } return -1; } responseBodyCacheSink.copyFrom(sink, sink.size() - bytesRead, bytesRead); return bytesRead; } @Override public Timeout timeout() { return responseBodySource.timeout(); } @Override public void close() throws IOException { if (closed) return; closed = true; if (discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) { responseBodySource.close(); commitCache(); } else { responseBodySource.close(); abortCacheQuietly(); } } private void commitCache() { try { responseBodyCacheSink.close(); cacheRecordEditor.commit(); } catch (Exception e) { closeQuietly(responseBodyCacheSink); abortCacheQuietly(); logger.e(e, "Failed to commit cache changes"); } } private void abortCacheQuietly() { closeQuietly(responseBodyCacheSink); try { cacheRecordEditor.abort(); } catch (Exception ignore) { logger.w(ignore, "Failed to abort cache edit"); } } } }