| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed 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 com.squareup.okhttp; |
| |
| import com.squareup.okhttp.internal.DiskLruCache; |
| import com.squareup.okhttp.internal.InternalCache; |
| import com.squareup.okhttp.internal.Util; |
| import com.squareup.okhttp.internal.http.CacheRequest; |
| import com.squareup.okhttp.internal.http.CacheStrategy; |
| import com.squareup.okhttp.internal.http.HttpMethod; |
| import com.squareup.okhttp.internal.http.OkHeaders; |
| import com.squareup.okhttp.internal.http.StatusLine; |
| import com.squareup.okhttp.internal.io.FileSystem; |
| import java.io.File; |
| import java.io.IOException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.NoSuchElementException; |
| import okio.Buffer; |
| import okio.BufferedSink; |
| import okio.BufferedSource; |
| import okio.ByteString; |
| import okio.ForwardingSink; |
| import okio.ForwardingSource; |
| import okio.Okio; |
| import okio.Sink; |
| import okio.Source; |
| |
| /** |
| * Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and |
| * bandwidth. |
| * |
| * <h3>Cache Optimization</h3> |
| * To measure cache effectiveness, this class tracks three statistics: |
| * <ul> |
| * <li><strong>{@linkplain #getRequestCount() Request Count:}</strong> the number of HTTP |
| * requests issued since this cache was created. |
| * <li><strong>{@linkplain #getNetworkCount() Network Count:}</strong> the number of those |
| * requests that required network use. |
| * <li><strong>{@linkplain #getHitCount() Hit Count:}</strong> the number of those requests whose |
| * responses were served by the cache. |
| * </ul> |
| * |
| * Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of |
| * the response, the client will issue a conditional {@code GET}. The server will then send either |
| * the updated response if it has changed, or a short 'not modified' response if the client's copy |
| * is still valid. Such responses increment both the network count and hit count. |
| * |
| * <p>The best way to improve the cache hit rate is by configuring the web server to return |
| * cacheable responses. Although this client honors all <a |
| * href="http://tools.ietf.org/html/rfc7234">HTTP/1.1 (RFC 7234)</a> cache headers, it doesn't cache |
| * partial responses. |
| * |
| * <h3>Force a Network Response</h3> |
| * In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip |
| * the cache, and fetch data directly from the server. To force a full refresh, add the {@code |
| * no-cache} directive: <pre> {@code |
| * |
| * Request request = new Request.Builder() |
| * .cacheControl(new CacheControl.Builder().noCache().build()) |
| * .url("http://publicobject.com/helloworld.txt") |
| * .build(); |
| * }</pre> |
| * |
| * If it is only necessary to force a cached response to be validated by the server, use the more |
| * efficient {@code max-age=0} directive instead: <pre> {@code |
| * |
| * Request request = new Request.Builder() |
| * .cacheControl(new CacheControl.Builder() |
| * .maxAge(0, TimeUnit.SECONDS) |
| * .build()) |
| * .url("http://publicobject.com/helloworld.txt") |
| * .build(); |
| * }</pre> |
| * |
| * <h3>Force a Cache Response</h3> |
| * Sometimes you'll want to show resources if they are available immediately, but not otherwise. |
| * This can be used so your application can show <i>something</i> while waiting for the latest data |
| * to be downloaded. To restrict a request to locally-cached resources, add the {@code |
| * only-if-cached} directive: <pre> {@code |
| * |
| * Request request = new Request.Builder() |
| * .cacheControl(new CacheControl.Builder() |
| * .onlyIfCached() |
| * .build()) |
| * .url("http://publicobject.com/helloworld.txt") |
| * .build(); |
| * Response forceCacheResponse = client.newCall(request).execute(); |
| * if (forceCacheResponse.code() != 504) { |
| * // The resource was cached! Show it. |
| * } else { |
| * // The resource was not cached. |
| * } |
| * }</pre> |
| * This technique works even better in situations where a stale response is better than no response. |
| * To permit stale cached responses, use the {@code max-stale} directive with the maximum staleness |
| * in seconds: <pre> {@code |
| * |
| * Request request = new Request.Builder() |
| * .cacheControl(new CacheControl.Builder() |
| * .maxStale(365, TimeUnit.DAYS) |
| * .build()) |
| * .url("http://publicobject.com/helloworld.txt") |
| * .build(); |
| * }</pre> |
| * |
| * <p>The {@link CacheControl} class can configure request caching directives and parse response |
| * caching directives. It even offers convenient constants {@link CacheControl#FORCE_NETWORK} and |
| * {@link CacheControl#FORCE_CACHE} that address the use cases above. |
| */ |
| public final class Cache { |
| private static final int VERSION = 201105; |
| private static final int ENTRY_METADATA = 0; |
| private static final int ENTRY_BODY = 1; |
| private static final int ENTRY_COUNT = 2; |
| |
| final InternalCache internalCache = new InternalCache() { |
| @Override public Response get(Request request) throws IOException { |
| return Cache.this.get(request); |
| } |
| @Override public CacheRequest put(Response response) throws IOException { |
| return Cache.this.put(response); |
| } |
| @Override public void remove(Request request) throws IOException { |
| Cache.this.remove(request); |
| } |
| @Override public void update(Response cached, Response network) throws IOException { |
| Cache.this.update(cached, network); |
| } |
| @Override public void trackConditionalCacheHit() { |
| Cache.this.trackConditionalCacheHit(); |
| } |
| @Override public void trackResponse(CacheStrategy cacheStrategy) { |
| Cache.this.trackResponse(cacheStrategy); |
| } |
| }; |
| |
| private final DiskLruCache cache; |
| |
| /* read and write statistics, all guarded by 'this' */ |
| private int writeSuccessCount; |
| private int writeAbortCount; |
| private int networkCount; |
| private int hitCount; |
| private int requestCount; |
| |
| public Cache(File directory, long maxSize) { |
| cache = DiskLruCache.create(FileSystem.SYSTEM, directory, VERSION, ENTRY_COUNT, maxSize); |
| } |
| |
| private static String urlToKey(Request request) { |
| return Util.md5Hex(request.urlString()); |
| } |
| |
| Response get(Request request) { |
| String key = urlToKey(request); |
| DiskLruCache.Snapshot snapshot; |
| Entry entry; |
| try { |
| snapshot = cache.get(key); |
| if (snapshot == null) { |
| return null; |
| } |
| } catch (IOException e) { |
| // Give up because the cache cannot be read. |
| return null; |
| } |
| |
| try { |
| entry = new Entry(snapshot.getSource(ENTRY_METADATA)); |
| } catch (IOException e) { |
| Util.closeQuietly(snapshot); |
| return null; |
| } |
| |
| Response response = entry.response(request, snapshot); |
| |
| if (!entry.matches(request, response)) { |
| Util.closeQuietly(response.body()); |
| return null; |
| } |
| |
| return response; |
| } |
| |
| private CacheRequest put(Response response) throws IOException { |
| String requestMethod = response.request().method(); |
| |
| if (HttpMethod.invalidatesCache(response.request().method())) { |
| try { |
| remove(response.request()); |
| } catch (IOException ignored) { |
| // The cache cannot be written. |
| } |
| return null; |
| } |
| if (!requestMethod.equals("GET")) { |
| // Don't cache non-GET responses. We're technically allowed to cache |
| // HEAD requests and some POST requests, but the complexity of doing |
| // so is high and the benefit is low. |
| return null; |
| } |
| |
| if (OkHeaders.hasVaryAll(response)) { |
| return null; |
| } |
| |
| Entry entry = new Entry(response); |
| DiskLruCache.Editor editor = null; |
| try { |
| editor = cache.edit(urlToKey(response.request())); |
| if (editor == null) { |
| return null; |
| } |
| entry.writeTo(editor); |
| return new CacheRequestImpl(editor); |
| } catch (IOException e) { |
| abortQuietly(editor); |
| return null; |
| } |
| } |
| |
| private void remove(Request request) throws IOException { |
| cache.remove(urlToKey(request)); |
| } |
| |
| private void update(Response cached, Response network) { |
| Entry entry = new Entry(network); |
| DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; |
| DiskLruCache.Editor editor = null; |
| try { |
| editor = snapshot.edit(); // Returns null if snapshot is not current. |
| if (editor != null) { |
| entry.writeTo(editor); |
| editor.commit(); |
| } |
| } catch (IOException e) { |
| abortQuietly(editor); |
| } |
| } |
| |
| private void abortQuietly(DiskLruCache.Editor editor) { |
| // Give up because the cache cannot be written. |
| try { |
| if (editor != null) { |
| editor.abort(); |
| } |
| } catch (IOException ignored) { |
| } |
| } |
| |
| /** |
| * Closes the cache and deletes all of its stored values. This will delete |
| * all files in the cache directory including files that weren't created by |
| * the cache. |
| */ |
| public void delete() throws IOException { |
| cache.delete(); |
| } |
| |
| /** |
| * Deletes all values stored in the cache. In-flight writes to the cache will |
| * complete normally, but the corresponding responses will not be stored. |
| */ |
| public void evictAll() throws IOException { |
| cache.evictAll(); |
| } |
| |
| /** |
| * Returns an iterator over the URLs in this cache. This iterator doesn't throw {@code |
| * ConcurrentModificationException}, but if new responses are added while iterating, their URLs |
| * will not be returned. If existing responses are evicted during iteration, they will be absent |
| * (unless they were already returned). |
| * |
| * <p>The iterator supports {@linkplain Iterator#remove}. Removing a URL from the iterator evicts |
| * the corresponding response from the cache. Use this to evict selected responses. |
| */ |
| public Iterator<String> urls() throws IOException { |
| return new Iterator<String>() { |
| final Iterator<DiskLruCache.Snapshot> delegate = cache.snapshots(); |
| |
| String nextUrl; |
| boolean canRemove; |
| |
| @Override public boolean hasNext() { |
| if (nextUrl != null) return true; |
| |
| canRemove = false; // Prevent delegate.remove() on the wrong item! |
| while (delegate.hasNext()) { |
| DiskLruCache.Snapshot snapshot = delegate.next(); |
| try { |
| BufferedSource metadata = Okio.buffer(snapshot.getSource(ENTRY_METADATA)); |
| nextUrl = metadata.readUtf8LineStrict(); |
| return true; |
| } catch (IOException ignored) { |
| // We couldn't read the metadata for this snapshot; possibly because the host filesystem |
| // has disappeared! Skip it. |
| } finally { |
| snapshot.close(); |
| } |
| } |
| |
| return false; |
| } |
| |
| @Override public String next() { |
| if (!hasNext()) throw new NoSuchElementException(); |
| String result = nextUrl; |
| nextUrl = null; |
| canRemove = true; |
| return result; |
| } |
| |
| @Override public void remove() { |
| if (!canRemove) throw new IllegalStateException("remove() before next()"); |
| delegate.remove(); |
| } |
| }; |
| } |
| |
| public synchronized int getWriteAbortCount() { |
| return writeAbortCount; |
| } |
| |
| public synchronized int getWriteSuccessCount() { |
| return writeSuccessCount; |
| } |
| |
| public long getSize() throws IOException { |
| return cache.size(); |
| } |
| |
| public long getMaxSize() { |
| return cache.getMaxSize(); |
| } |
| |
| public void flush() throws IOException { |
| cache.flush(); |
| } |
| |
| public void close() throws IOException { |
| cache.close(); |
| } |
| |
| public File getDirectory() { |
| return cache.getDirectory(); |
| } |
| |
| public boolean isClosed() { |
| return cache.isClosed(); |
| } |
| |
| private synchronized void trackResponse(CacheStrategy cacheStrategy) { |
| requestCount++; |
| |
| if (cacheStrategy.networkRequest != null) { |
| // If this is a conditional request, we'll increment hitCount if/when it hits. |
| networkCount++; |
| |
| } else if (cacheStrategy.cacheResponse != null) { |
| // This response uses the cache and not the network. That's a cache hit. |
| hitCount++; |
| } |
| } |
| |
| private synchronized void trackConditionalCacheHit() { |
| hitCount++; |
| } |
| |
| public synchronized int getNetworkCount() { |
| return networkCount; |
| } |
| |
| public synchronized int getHitCount() { |
| return hitCount; |
| } |
| |
| public synchronized int getRequestCount() { |
| return requestCount; |
| } |
| |
| private final class CacheRequestImpl implements CacheRequest { |
| private final DiskLruCache.Editor editor; |
| private Sink cacheOut; |
| private boolean done; |
| private Sink body; |
| |
| public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException { |
| this.editor = editor; |
| this.cacheOut = editor.newSink(ENTRY_BODY); |
| this.body = new ForwardingSink(cacheOut) { |
| @Override public void close() throws IOException { |
| synchronized (Cache.this) { |
| if (done) { |
| return; |
| } |
| done = true; |
| writeSuccessCount++; |
| } |
| super.close(); |
| editor.commit(); |
| } |
| }; |
| } |
| |
| @Override public void abort() { |
| synchronized (Cache.this) { |
| if (done) { |
| return; |
| } |
| done = true; |
| writeAbortCount++; |
| } |
| Util.closeQuietly(cacheOut); |
| try { |
| editor.abort(); |
| } catch (IOException ignored) { |
| } |
| } |
| |
| @Override public Sink body() { |
| return body; |
| } |
| } |
| |
| private static final class Entry { |
| private final String url; |
| private final Headers varyHeaders; |
| private final String requestMethod; |
| private final Protocol protocol; |
| private final int code; |
| private final String message; |
| private final Headers responseHeaders; |
| private final Handshake handshake; |
| |
| /** |
| * Reads an entry from an input stream. A typical entry looks like this: |
| * <pre>{@code |
| * http://google.com/foo |
| * GET |
| * 2 |
| * Accept-Language: fr-CA |
| * Accept-Charset: UTF-8 |
| * HTTP/1.1 200 OK |
| * 3 |
| * Content-Type: image/png |
| * Content-Length: 100 |
| * Cache-Control: max-age=600 |
| * }</pre> |
| * |
| * <p>A typical HTTPS file looks like this: |
| * <pre>{@code |
| * https://google.com/foo |
| * GET |
| * 2 |
| * Accept-Language: fr-CA |
| * Accept-Charset: UTF-8 |
| * HTTP/1.1 200 OK |
| * 3 |
| * Content-Type: image/png |
| * Content-Length: 100 |
| * Cache-Control: max-age=600 |
| * |
| * AES_256_WITH_MD5 |
| * 2 |
| * base64-encoded peerCertificate[0] |
| * base64-encoded peerCertificate[1] |
| * -1 |
| * }</pre> |
| * The file is newline separated. The first two lines are the URL and |
| * the request method. Next is the number of HTTP Vary request header |
| * lines, followed by those lines. |
| * |
| * <p>Next is the response status line, followed by the number of HTTP |
| * response header lines, followed by those lines. |
| * |
| * <p>HTTPS responses also contain SSL session information. This begins |
| * with a blank line, and then a line containing the cipher suite. Next |
| * is the length of the peer certificate chain. These certificates are |
| * base64-encoded and appear each on their own line. The next line |
| * contains the length of the local certificate chain. These |
| * certificates are also base64-encoded and appear each on their own |
| * line. A length of -1 is used to encode a null array. |
| */ |
| public Entry(Source in) throws IOException { |
| try { |
| BufferedSource source = Okio.buffer(in); |
| url = source.readUtf8LineStrict(); |
| requestMethod = source.readUtf8LineStrict(); |
| Headers.Builder varyHeadersBuilder = new Headers.Builder(); |
| int varyRequestHeaderLineCount = readInt(source); |
| for (int i = 0; i < varyRequestHeaderLineCount; i++) { |
| varyHeadersBuilder.addLenient(source.readUtf8LineStrict()); |
| } |
| varyHeaders = varyHeadersBuilder.build(); |
| |
| StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict()); |
| protocol = statusLine.protocol; |
| code = statusLine.code; |
| message = statusLine.message; |
| Headers.Builder responseHeadersBuilder = new Headers.Builder(); |
| int responseHeaderLineCount = readInt(source); |
| for (int i = 0; i < responseHeaderLineCount; i++) { |
| responseHeadersBuilder.addLenient(source.readUtf8LineStrict()); |
| } |
| responseHeaders = responseHeadersBuilder.build(); |
| |
| if (isHttps()) { |
| String blank = source.readUtf8LineStrict(); |
| if (blank.length() > 0) { |
| throw new IOException("expected \"\" but was \"" + blank + "\""); |
| } |
| String cipherSuite = source.readUtf8LineStrict(); |
| List<Certificate> peerCertificates = readCertificateList(source); |
| List<Certificate> localCertificates = readCertificateList(source); |
| handshake = Handshake.get(cipherSuite, peerCertificates, localCertificates); |
| } else { |
| handshake = null; |
| } |
| } finally { |
| in.close(); |
| } |
| } |
| |
| public Entry(Response response) { |
| this.url = response.request().urlString(); |
| this.varyHeaders = OkHeaders.varyHeaders(response); |
| this.requestMethod = response.request().method(); |
| this.protocol = response.protocol(); |
| this.code = response.code(); |
| this.message = response.message(); |
| this.responseHeaders = response.headers(); |
| this.handshake = response.handshake(); |
| } |
| |
| public void writeTo(DiskLruCache.Editor editor) throws IOException { |
| BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA)); |
| |
| sink.writeUtf8(url); |
| sink.writeByte('\n'); |
| sink.writeUtf8(requestMethod); |
| sink.writeByte('\n'); |
| sink.writeDecimalLong(varyHeaders.size()); |
| sink.writeByte('\n'); |
| for (int i = 0, size = varyHeaders.size(); i < size; i++) { |
| sink.writeUtf8(varyHeaders.name(i)); |
| sink.writeUtf8(": "); |
| sink.writeUtf8(varyHeaders.value(i)); |
| sink.writeByte('\n'); |
| } |
| |
| sink.writeUtf8(new StatusLine(protocol, code, message).toString()); |
| sink.writeByte('\n'); |
| sink.writeDecimalLong(responseHeaders.size()); |
| sink.writeByte('\n'); |
| for (int i = 0, size = responseHeaders.size(); i < size; i++) { |
| sink.writeUtf8(responseHeaders.name(i)); |
| sink.writeUtf8(": "); |
| sink.writeUtf8(responseHeaders.value(i)); |
| sink.writeByte('\n'); |
| } |
| |
| if (isHttps()) { |
| sink.writeByte('\n'); |
| sink.writeUtf8(handshake.cipherSuite()); |
| sink.writeByte('\n'); |
| writeCertList(sink, handshake.peerCertificates()); |
| writeCertList(sink, handshake.localCertificates()); |
| } |
| sink.close(); |
| } |
| |
| private boolean isHttps() { |
| return url.startsWith("https://"); |
| } |
| |
| private List<Certificate> readCertificateList(BufferedSource source) throws IOException { |
| int length = readInt(source); |
| if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null. |
| |
| try { |
| CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); |
| List<Certificate> result = new ArrayList<>(length); |
| for (int i = 0; i < length; i++) { |
| String line = source.readUtf8LineStrict(); |
| Buffer bytes = new Buffer(); |
| bytes.write(ByteString.decodeBase64(line)); |
| result.add(certificateFactory.generateCertificate(bytes.inputStream())); |
| } |
| return result; |
| } catch (CertificateException e) { |
| throw new IOException(e.getMessage()); |
| } |
| } |
| |
| private void writeCertList(BufferedSink sink, List<Certificate> certificates) |
| throws IOException { |
| try { |
| sink.writeDecimalLong(certificates.size()); |
| sink.writeByte('\n'); |
| for (int i = 0, size = certificates.size(); i < size; i++) { |
| byte[] bytes = certificates.get(i).getEncoded(); |
| String line = ByteString.of(bytes).base64(); |
| sink.writeUtf8(line); |
| sink.writeByte('\n'); |
| } |
| } catch (CertificateEncodingException e) { |
| throw new IOException(e.getMessage()); |
| } |
| } |
| |
| public boolean matches(Request request, Response response) { |
| return url.equals(request.urlString()) |
| && requestMethod.equals(request.method()) |
| && OkHeaders.varyMatches(response, varyHeaders, request); |
| } |
| |
| public Response response(Request request, DiskLruCache.Snapshot snapshot) { |
| String contentType = responseHeaders.get("Content-Type"); |
| String contentLength = responseHeaders.get("Content-Length"); |
| Request cacheRequest = new Request.Builder() |
| .url(url) |
| .method(requestMethod, null) |
| .headers(varyHeaders) |
| .build(); |
| return new Response.Builder() |
| .request(cacheRequest) |
| .protocol(protocol) |
| .code(code) |
| .message(message) |
| .headers(responseHeaders) |
| .body(new CacheResponseBody(snapshot, contentType, contentLength)) |
| .handshake(handshake) |
| .build(); |
| } |
| } |
| |
| private static int readInt(BufferedSource source) throws IOException { |
| try { |
| long result = source.readDecimalLong(); |
| String line = source.readUtf8LineStrict(); |
| if (result < 0 || result > Integer.MAX_VALUE || !line.isEmpty()) { |
| throw new IOException("expected an int but was \"" + result + line + "\""); |
| } |
| return (int) result; |
| } catch (NumberFormatException e) { |
| throw new IOException(e.getMessage()); |
| } |
| } |
| |
| private static class CacheResponseBody extends ResponseBody { |
| private final DiskLruCache.Snapshot snapshot; |
| private final BufferedSource bodySource; |
| private final String contentType; |
| private final String contentLength; |
| |
| public CacheResponseBody(final DiskLruCache.Snapshot snapshot, |
| String contentType, String contentLength) { |
| this.snapshot = snapshot; |
| this.contentType = contentType; |
| this.contentLength = contentLength; |
| |
| Source source = snapshot.getSource(ENTRY_BODY); |
| bodySource = Okio.buffer(new ForwardingSource(source) { |
| @Override public void close() throws IOException { |
| snapshot.close(); |
| super.close(); |
| } |
| }); |
| } |
| |
| @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 bodySource; |
| } |
| } |
| } |