/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.cache; import org.wikipedia.dataclient.okhttp.cache.CacheDelegateStrategy; import java.io.IOException; import okhttp3.Headers; import okhttp3.Interceptor; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import okhttp3.internal.Internal; import okhttp3.internal.Util; import okhttp3.internal.http.HttpCodec; import okhttp3.internal.http.HttpHeaders; import okhttp3.internal.http.HttpMethod; import okhttp3.internal.http.RealResponseBody; import okio.Buffer; import okio.BufferedSink; import okio.BufferedSource; import okio.Okio; import okio.Sink; import okio.Source; import okio.Timeout; import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static okhttp3.internal.Util.closeQuietly; import static okhttp3.internal.Util.discard; /** Serves requests from the cache and writes responses to the cache. Copied from https://github.com/square/okhttp/blob/a27afaf/okhttp/src/main/java/okhttp3/internal/cache/CacheInterceptor.java to allow a custom cache strategy. Deviations are marked with "Change:". */ public final class CacheDelegateInterceptor implements Interceptor { final InternalCache cache; // Change: add secondaryCache member variable final InternalCache secondaryCache; // Change: add secondaryCache param public CacheDelegateInterceptor(InternalCache cache, InternalCache secondaryCache) { this.cache = cache; // Change: initialize secondaryCache param this.secondaryCache = secondaryCache; } @Override public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache != null ? cache.get(chain.request()) : null; long now = System.currentTimeMillis(); CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse; if (cache != null) { cache.trackResponse(strategy); } // Change: the true cache further down the chain must be used directly to retrieve non-network // responses if (networkRequest == null && cacheResponse == null) { cacheResponse = secondaryCache.get(chain.request()); } if (cacheCandidate != null && cacheResponse == null) { closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it. } // If we're forbidden from using the network and the cache is insufficient, fail. if (networkRequest == null && cacheResponse == null) { return new Response.Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(Util.EMPTY_RESPONSE) .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); } // If we don't need the network, we're done. if (networkRequest == null) { return cacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); } Response networkResponse = null; try { networkResponse = chain.proceed(networkRequest); } finally { // If we're crashing on I/O or otherwise, don't leak the cache body. if (networkResponse == null && cacheCandidate != null) { closeQuietly(cacheCandidate.body()); } } // Change: the original CacheInterceptor sets Response.Builder.*Response() but Response.Builder, // invoked further down for the final Response, forbids a Response's cache / network / prior // Response to itself contain a cache / network / prior Response or a body. The body is already // stripped by stripBody() networkResponse = networkResponse.newBuilder() .cacheResponse(null) .networkResponse(null) .priorResponse(null) .build(); // If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { if (networkResponse.code() == HTTP_NOT_MODIFIED) { Response response = cacheResponse.newBuilder() .headers(combine(cacheResponse.headers(), networkResponse.headers())) .sentRequestAtMillis(networkResponse.sentRequestAtMillis()) .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); networkResponse.body().close(); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). cache.trackConditionalCacheHit(); cache.update(cacheResponse, response); return response; } else { closeQuietly(cacheResponse.body()); } } Response response = networkResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (HttpHeaders.hasBody(response)) { // Change: add cacheCandidate parameter CacheRequest cacheRequest = maybeCache(cacheCandidate, response, networkResponse.request(), cache); response = cacheWritingResponse(cacheRequest, response); } return response; } private static Response stripBody(Response response) { return response != null && response.body() != null ? response.newBuilder().body(null).build() : response; } // Change: add cacheCandidate parameter private CacheRequest maybeCache(Response cacheCandidate, Response userResponse, Request networkRequest, InternalCache responseCache) throws IOException { if (responseCache == null) return null; // Should we cache this response for this request? if (!CacheStrategy.isCacheable(userResponse, networkRequest)) { if (HttpMethod.invalidatesCache(networkRequest.method())) { try { responseCache.remove(networkRequest); } catch (IOException ignored) { // The cache cannot be written. } } return null; } // Change: do not permit cache writes unless the page has been cached previously or the request // is cacheable if (cacheCandidate == null && !CacheDelegateStrategy.isCacheable(networkRequest)) { return null; } // Offer this request to the cache. return responseCache.put(userResponse); } /** * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we * may never exhaust the source stream and therefore not complete the cached response. */ private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response) throws IOException { // Some apps return a null body; for compatibility we treat that like a null cache request. if (cacheRequest == null) return response; Sink cacheBodyUnbuffered = cacheRequest.body(); if (cacheBodyUnbuffered == null) return response; final BufferedSource source = response.body().source(); final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered); Source cacheWritingSource = new Source() { boolean cacheRequestClosed; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead; try { bytesRead = source.read(sink, byteCount); } catch (IOException e) { if (!cacheRequestClosed) { cacheRequestClosed = true; cacheRequest.abort(); // Failed to write a complete cache response. } throw e; } if (bytesRead == -1) { if (!cacheRequestClosed) { cacheRequestClosed = true; cacheBody.close(); // The cache response is complete! } return -1; } sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead); cacheBody.emitCompleteSegments(); return bytesRead; } @Override public Timeout timeout() { return source.timeout(); } @Override public void close() throws IOException { if (!cacheRequestClosed && !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) { cacheRequestClosed = true; cacheRequest.abort(); } source.close(); } }; return response.newBuilder() .body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource))) .build(); } /** Combines cached headers with a network headers as defined by RFC 2616, 13.5.3. */ private static Headers combine(Headers cachedHeaders, Headers networkHeaders) { Headers.Builder result = new Headers.Builder(); for (int i = 0, size = cachedHeaders.size(); i < size; i++) { String fieldName = cachedHeaders.name(i); String value = cachedHeaders.value(i); if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) { continue; // Drop 100-level freshness warnings. } if (!isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) { Internal.instance.addLenient(result, fieldName, value); } } for (int i = 0, size = networkHeaders.size(); i < size; i++) { String fieldName = networkHeaders.name(i); if ("Content-Length".equalsIgnoreCase(fieldName)) { continue; // Ignore content-length headers of validating responses. } if (isEndToEnd(fieldName)) { Internal.instance.addLenient(result, fieldName, networkHeaders.value(i)); } } return result.build(); } /** * Returns true if {@code fieldName} is an end-to-end HTTP header, as defined by RFC 2616, * 13.5.1. */ static boolean isEndToEnd(String fieldName) { return !"Connection".equalsIgnoreCase(fieldName) && !"Keep-Alive".equalsIgnoreCase(fieldName) && !"Proxy-Authenticate".equalsIgnoreCase(fieldName) && !"Proxy-Authorization".equalsIgnoreCase(fieldName) && !"TE".equalsIgnoreCase(fieldName) && !"Trailers".equalsIgnoreCase(fieldName) && !"Transfer-Encoding".equalsIgnoreCase(fieldName) && !"Upgrade".equalsIgnoreCase(fieldName); } }