Home | History | Annotate | Download | only in okhttp
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.squareup.okhttp;
     18 
     19 import com.squareup.okhttp.internal.DiskLruCache;
     20 import com.squareup.okhttp.internal.InternalCache;
     21 import com.squareup.okhttp.internal.Util;
     22 import com.squareup.okhttp.internal.http.CacheRequest;
     23 import com.squareup.okhttp.internal.http.CacheStrategy;
     24 import com.squareup.okhttp.internal.http.HttpMethod;
     25 import com.squareup.okhttp.internal.http.OkHeaders;
     26 import com.squareup.okhttp.internal.http.StatusLine;
     27 import com.squareup.okhttp.internal.io.FileSystem;
     28 import java.io.File;
     29 import java.io.IOException;
     30 import java.security.cert.Certificate;
     31 import java.security.cert.CertificateEncodingException;
     32 import java.security.cert.CertificateException;
     33 import java.security.cert.CertificateFactory;
     34 import java.util.ArrayList;
     35 import java.util.Collections;
     36 import java.util.Iterator;
     37 import java.util.List;
     38 import java.util.NoSuchElementException;
     39 import okio.Buffer;
     40 import okio.BufferedSink;
     41 import okio.BufferedSource;
     42 import okio.ByteString;
     43 import okio.ForwardingSink;
     44 import okio.ForwardingSource;
     45 import okio.Okio;
     46 import okio.Sink;
     47 import okio.Source;
     48 
     49 /**
     50  * Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and
     51  * bandwidth.
     52  *
     53  * <h3>Cache Optimization</h3>
     54  * To measure cache effectiveness, this class tracks three statistics:
     55  * <ul>
     56  *   <li><strong>{@linkplain #getRequestCount() Request Count:}</strong> the number of HTTP
     57  *     requests issued since this cache was created.
     58  *   <li><strong>{@linkplain #getNetworkCount() Network Count:}</strong> the number of those
     59  *     requests that required network use.
     60  *   <li><strong>{@linkplain #getHitCount() Hit Count:}</strong> the number of those requests whose
     61  *     responses were served by the cache.
     62  * </ul>
     63  *
     64  * Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of
     65  * the response, the client will issue a conditional {@code GET}. The server will then send either
     66  * the updated response if it has changed, or a short 'not modified' response if the client's copy
     67  * is still valid. Such responses increment both the network count and hit count.
     68  *
     69  * <p>The best way to improve the cache hit rate is by configuring the web server to return
     70  * cacheable responses. Although this client honors all <a
     71  * href="http://tools.ietf.org/html/rfc7234">HTTP/1.1 (RFC 7234)</a> cache headers, it doesn't cache
     72  * partial responses.
     73  *
     74  * <h3>Force a Network Response</h3>
     75  * In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip
     76  * the cache, and fetch data directly from the server. To force a full refresh, add the {@code
     77  * no-cache} directive: <pre>   {@code
     78  *
     79  *   Request request = new Request.Builder()
     80  *       .cacheControl(new CacheControl.Builder().noCache().build())
     81  *       .url("http://publicobject.com/helloworld.txt")
     82  *       .build();
     83  * }</pre>
     84  *
     85  * If it is only necessary to force a cached response to be validated by the server, use the more
     86  * efficient {@code max-age=0} directive instead: <pre>   {@code
     87  *
     88  *   Request request = new Request.Builder()
     89  *       .cacheControl(new CacheControl.Builder()
     90  *           .maxAge(0, TimeUnit.SECONDS)
     91  *           .build())
     92  *       .url("http://publicobject.com/helloworld.txt")
     93  *       .build();
     94  * }</pre>
     95  *
     96  * <h3>Force a Cache Response</h3>
     97  * Sometimes you'll want to show resources if they are available immediately, but not otherwise.
     98  * This can be used so your application can show <i>something</i> while waiting for the latest data
     99  * to be downloaded. To restrict a request to locally-cached resources, add the {@code
    100  * only-if-cached} directive: <pre>   {@code
    101  *
    102  *     Request request = new Request.Builder()
    103  *         .cacheControl(new CacheControl.Builder()
    104  *             .onlyIfCached()
    105  *             .build())
    106  *         .url("http://publicobject.com/helloworld.txt")
    107  *         .build();
    108  *     Response forceCacheResponse = client.newCall(request).execute();
    109  *     if (forceCacheResponse.code() != 504) {
    110  *       // The resource was cached! Show it.
    111  *     } else {
    112  *       // The resource was not cached.
    113  *     }
    114  * }</pre>
    115  * This technique works even better in situations where a stale response is better than no response.
    116  * To permit stale cached responses, use the {@code max-stale} directive with the maximum staleness
    117  * in seconds: <pre>   {@code
    118  *
    119  *   Request request = new Request.Builder()
    120  *       .cacheControl(new CacheControl.Builder()
    121  *           .maxStale(365, TimeUnit.DAYS)
    122  *           .build())
    123  *       .url("http://publicobject.com/helloworld.txt")
    124  *       .build();
    125  * }</pre>
    126  *
    127  * <p>The {@link CacheControl} class can configure request caching directives and parse response
    128  * caching directives. It even offers convenient constants {@link CacheControl#FORCE_NETWORK} and
    129  * {@link CacheControl#FORCE_CACHE} that address the use cases above.
    130  */
    131 public final class Cache {
    132   private static final int VERSION = 201105;
    133   private static final int ENTRY_METADATA = 0;
    134   private static final int ENTRY_BODY = 1;
    135   private static final int ENTRY_COUNT = 2;
    136 
    137   final InternalCache internalCache = new InternalCache() {
    138     @Override public Response get(Request request) throws IOException {
    139       return Cache.this.get(request);
    140     }
    141     @Override public CacheRequest put(Response response) throws IOException {
    142       return Cache.this.put(response);
    143     }
    144     @Override public void remove(Request request) throws IOException {
    145       Cache.this.remove(request);
    146     }
    147     @Override public void update(Response cached, Response network) throws IOException {
    148       Cache.this.update(cached, network);
    149     }
    150     @Override public void trackConditionalCacheHit() {
    151       Cache.this.trackConditionalCacheHit();
    152     }
    153     @Override public void trackResponse(CacheStrategy cacheStrategy) {
    154       Cache.this.trackResponse(cacheStrategy);
    155     }
    156   };
    157 
    158   private final DiskLruCache cache;
    159 
    160   /* read and write statistics, all guarded by 'this' */
    161   private int writeSuccessCount;
    162   private int writeAbortCount;
    163   private int networkCount;
    164   private int hitCount;
    165   private int requestCount;
    166 
    167   public Cache(File directory, long maxSize) {
    168     this(directory, maxSize, FileSystem.SYSTEM);
    169   }
    170 
    171   Cache(File directory, long maxSize, FileSystem fileSystem) {
    172     this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
    173   }
    174 
    175   private static String urlToKey(Request request) {
    176     return Util.md5Hex(request.urlString());
    177   }
    178 
    179   Response get(Request request) {
    180     String key = urlToKey(request);
    181     DiskLruCache.Snapshot snapshot;
    182     Entry entry;
    183     try {
    184       snapshot = cache.get(key);
    185       if (snapshot == null) {
    186         return null;
    187       }
    188     } catch (IOException e) {
    189       // Give up because the cache cannot be read.
    190       return null;
    191     }
    192 
    193     try {
    194       entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    195     } catch (IOException e) {
    196       Util.closeQuietly(snapshot);
    197       return null;
    198     }
    199 
    200     Response response = entry.response(request, snapshot);
    201 
    202     if (!entry.matches(request, response)) {
    203       Util.closeQuietly(response.body());
    204       return null;
    205     }
    206 
    207     return response;
    208   }
    209 
    210   private CacheRequest put(Response response) throws IOException {
    211     String requestMethod = response.request().method();
    212 
    213     if (HttpMethod.invalidatesCache(response.request().method())) {
    214       try {
    215         remove(response.request());
    216       } catch (IOException ignored) {
    217         // The cache cannot be written.
    218       }
    219       return null;
    220     }
    221     if (!requestMethod.equals("GET")) {
    222       // Don't cache non-GET responses. We're technically allowed to cache
    223       // HEAD requests and some POST requests, but the complexity of doing
    224       // so is high and the benefit is low.
    225       return null;
    226     }
    227 
    228     if (OkHeaders.hasVaryAll(response)) {
    229       return null;
    230     }
    231 
    232     Entry entry = new Entry(response);
    233     DiskLruCache.Editor editor = null;
    234     try {
    235       editor = cache.edit(urlToKey(response.request()));
    236       if (editor == null) {
    237         return null;
    238       }
    239       entry.writeTo(editor);
    240       return new CacheRequestImpl(editor);
    241     } catch (IOException e) {
    242       abortQuietly(editor);
    243       return null;
    244     }
    245   }
    246 
    247   private void remove(Request request) throws IOException {
    248     cache.remove(urlToKey(request));
    249   }
    250 
    251   private void update(Response cached, Response network) {
    252     Entry entry = new Entry(network);
    253     DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    254     DiskLruCache.Editor editor = null;
    255     try {
    256       editor = snapshot.edit(); // Returns null if snapshot is not current.
    257       if (editor != null) {
    258         entry.writeTo(editor);
    259         editor.commit();
    260       }
    261     } catch (IOException e) {
    262       abortQuietly(editor);
    263     }
    264   }
    265 
    266   private void abortQuietly(DiskLruCache.Editor editor) {
    267     // Give up because the cache cannot be written.
    268     try {
    269       if (editor != null) {
    270         editor.abort();
    271       }
    272     } catch (IOException ignored) {
    273     }
    274   }
    275 
    276   /**
    277    * Initialize the cache. This will include reading the journal files from
    278    * the storage and building up the necessary in-memory cache information.
    279    * <p>
    280    * The initialization time may vary depending on the journal file size and
    281    * the current actual cache size. The application needs to be aware of calling
    282    * this function during the initialization phase and preferably in a background
    283    * worker thread.
    284    * <p>
    285    * Note that if the application chooses to not call this method to initialize
    286    * the cache. By default, the okhttp will perform lazy initialization upon the
    287    * first usage of the cache.
    288    */
    289   public void initialize() throws IOException {
    290     cache.initialize();
    291   }
    292 
    293   /**
    294    * Closes the cache and deletes all of its stored values. This will delete
    295    * all files in the cache directory including files that weren't created by
    296    * the cache.
    297    */
    298   public void delete() throws IOException {
    299     cache.delete();
    300   }
    301 
    302   /**
    303    * Deletes all values stored in the cache. In-flight writes to the cache will
    304    * complete normally, but the corresponding responses will not be stored.
    305    */
    306   public void evictAll() throws IOException {
    307     cache.evictAll();
    308   }
    309 
    310   /**
    311    * Returns an iterator over the URLs in this cache. This iterator doesn't throw {@code
    312    * ConcurrentModificationException}, but if new responses are added while iterating, their URLs
    313    * will not be returned. If existing responses are evicted during iteration, they will be absent
    314    * (unless they were already returned).
    315    *
    316    * <p>The iterator supports {@linkplain Iterator#remove}. Removing a URL from the iterator evicts
    317    * the corresponding response from the cache. Use this to evict selected responses.
    318    */
    319   public Iterator<String> urls() throws IOException {
    320     return new Iterator<String>() {
    321       final Iterator<DiskLruCache.Snapshot> delegate = cache.snapshots();
    322 
    323       String nextUrl;
    324       boolean canRemove;
    325 
    326       @Override public boolean hasNext() {
    327         if (nextUrl != null) return true;
    328 
    329         canRemove = false; // Prevent delegate.remove() on the wrong item!
    330         while (delegate.hasNext()) {
    331           DiskLruCache.Snapshot snapshot = delegate.next();
    332           try {
    333             BufferedSource metadata = Okio.buffer(snapshot.getSource(ENTRY_METADATA));
    334             nextUrl = metadata.readUtf8LineStrict();
    335             return true;
    336           } catch (IOException ignored) {
    337             // We couldn't read the metadata for this snapshot; possibly because the host filesystem
    338             // has disappeared! Skip it.
    339           } finally {
    340             snapshot.close();
    341           }
    342         }
    343 
    344         return false;
    345       }
    346 
    347       @Override public String next() {
    348         if (!hasNext()) throw new NoSuchElementException();
    349         String result = nextUrl;
    350         nextUrl = null;
    351         canRemove = true;
    352         return result;
    353       }
    354 
    355       @Override public void remove() {
    356         if (!canRemove) throw new IllegalStateException("remove() before next()");
    357         delegate.remove();
    358       }
    359     };
    360   }
    361 
    362   public synchronized int getWriteAbortCount() {
    363     return writeAbortCount;
    364   }
    365 
    366   public synchronized int getWriteSuccessCount() {
    367     return writeSuccessCount;
    368   }
    369 
    370   public long getSize() throws IOException {
    371     return cache.size();
    372   }
    373 
    374   public long getMaxSize() {
    375     return cache.getMaxSize();
    376   }
    377 
    378   public void flush() throws IOException {
    379     cache.flush();
    380   }
    381 
    382   public void close() throws IOException {
    383     cache.close();
    384   }
    385 
    386   public File getDirectory() {
    387     return cache.getDirectory();
    388   }
    389 
    390   public boolean isClosed() {
    391     return cache.isClosed();
    392   }
    393 
    394   private synchronized void trackResponse(CacheStrategy cacheStrategy) {
    395     requestCount++;
    396 
    397     if (cacheStrategy.networkRequest != null) {
    398       // If this is a conditional request, we'll increment hitCount if/when it hits.
    399       networkCount++;
    400 
    401     } else if (cacheStrategy.cacheResponse != null) {
    402       // This response uses the cache and not the network. That's a cache hit.
    403       hitCount++;
    404     }
    405   }
    406 
    407   private synchronized void trackConditionalCacheHit() {
    408     hitCount++;
    409   }
    410 
    411   public synchronized int getNetworkCount() {
    412     return networkCount;
    413   }
    414 
    415   public synchronized int getHitCount() {
    416     return hitCount;
    417   }
    418 
    419   public synchronized int getRequestCount() {
    420     return requestCount;
    421   }
    422 
    423   private final class CacheRequestImpl implements CacheRequest {
    424     private final DiskLruCache.Editor editor;
    425     private Sink cacheOut;
    426     private boolean done;
    427     private Sink body;
    428 
    429     public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
    430       this.editor = editor;
    431       this.cacheOut = editor.newSink(ENTRY_BODY);
    432       this.body = new ForwardingSink(cacheOut) {
    433         @Override public void close() throws IOException {
    434           synchronized (Cache.this) {
    435             if (done) {
    436               return;
    437             }
    438             done = true;
    439             writeSuccessCount++;
    440           }
    441           super.close();
    442           editor.commit();
    443         }
    444       };
    445     }
    446 
    447     @Override public void abort() {
    448       synchronized (Cache.this) {
    449         if (done) {
    450           return;
    451         }
    452         done = true;
    453         writeAbortCount++;
    454       }
    455       Util.closeQuietly(cacheOut);
    456       try {
    457         editor.abort();
    458       } catch (IOException ignored) {
    459       }
    460     }
    461 
    462     @Override public Sink body() {
    463       return body;
    464     }
    465   }
    466 
    467   private static final class Entry {
    468     private final String url;
    469     private final Headers varyHeaders;
    470     private final String requestMethod;
    471     private final Protocol protocol;
    472     private final int code;
    473     private final String message;
    474     private final Headers responseHeaders;
    475     private final Handshake handshake;
    476 
    477     /**
    478      * Reads an entry from an input stream. A typical entry looks like this:
    479      * <pre>{@code
    480      *   http://google.com/foo
    481      *   GET
    482      *   2
    483      *   Accept-Language: fr-CA
    484      *   Accept-Charset: UTF-8
    485      *   HTTP/1.1 200 OK
    486      *   3
    487      *   Content-Type: image/png
    488      *   Content-Length: 100
    489      *   Cache-Control: max-age=600
    490      * }</pre>
    491      *
    492      * <p>A typical HTTPS file looks like this:
    493      * <pre>{@code
    494      *   https://google.com/foo
    495      *   GET
    496      *   2
    497      *   Accept-Language: fr-CA
    498      *   Accept-Charset: UTF-8
    499      *   HTTP/1.1 200 OK
    500      *   3
    501      *   Content-Type: image/png
    502      *   Content-Length: 100
    503      *   Cache-Control: max-age=600
    504      *
    505      *   AES_256_WITH_MD5
    506      *   2
    507      *   base64-encoded peerCertificate[0]
    508      *   base64-encoded peerCertificate[1]
    509      *   -1
    510      * }</pre>
    511      * The file is newline separated. The first two lines are the URL and
    512      * the request method. Next is the number of HTTP Vary request header
    513      * lines, followed by those lines.
    514      *
    515      * <p>Next is the response status line, followed by the number of HTTP
    516      * response header lines, followed by those lines.
    517      *
    518      * <p>HTTPS responses also contain SSL session information. This begins
    519      * with a blank line, and then a line containing the cipher suite. Next
    520      * is the length of the peer certificate chain. These certificates are
    521      * base64-encoded and appear each on their own line. The next line
    522      * contains the length of the local certificate chain. These
    523      * certificates are also base64-encoded and appear each on their own
    524      * line. A length of -1 is used to encode a null array.
    525      */
    526     public Entry(Source in) throws IOException {
    527       try {
    528         BufferedSource source = Okio.buffer(in);
    529         url = source.readUtf8LineStrict();
    530         requestMethod = source.readUtf8LineStrict();
    531         Headers.Builder varyHeadersBuilder = new Headers.Builder();
    532         int varyRequestHeaderLineCount = readInt(source);
    533         for (int i = 0; i < varyRequestHeaderLineCount; i++) {
    534           varyHeadersBuilder.addLenient(source.readUtf8LineStrict());
    535         }
    536         varyHeaders = varyHeadersBuilder.build();
    537 
    538         StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
    539         protocol = statusLine.protocol;
    540         code = statusLine.code;
    541         message = statusLine.message;
    542         Headers.Builder responseHeadersBuilder = new Headers.Builder();
    543         int responseHeaderLineCount = readInt(source);
    544         for (int i = 0; i < responseHeaderLineCount; i++) {
    545           responseHeadersBuilder.addLenient(source.readUtf8LineStrict());
    546         }
    547         responseHeaders = responseHeadersBuilder.build();
    548 
    549         if (isHttps()) {
    550           String blank = source.readUtf8LineStrict();
    551           if (blank.length() > 0) {
    552             throw new IOException("expected \"\" but was \"" + blank + "\"");
    553           }
    554           String cipherSuite = source.readUtf8LineStrict();
    555           List<Certificate> peerCertificates = readCertificateList(source);
    556           List<Certificate> localCertificates = readCertificateList(source);
    557           handshake = Handshake.get(cipherSuite, peerCertificates, localCertificates);
    558         } else {
    559           handshake = null;
    560         }
    561       } finally {
    562         in.close();
    563       }
    564     }
    565 
    566     public Entry(Response response) {
    567       this.url = response.request().urlString();
    568       this.varyHeaders = OkHeaders.varyHeaders(response);
    569       this.requestMethod = response.request().method();
    570       this.protocol = response.protocol();
    571       this.code = response.code();
    572       this.message = response.message();
    573       this.responseHeaders = response.headers();
    574       this.handshake = response.handshake();
    575     }
    576 
    577     public void writeTo(DiskLruCache.Editor editor) throws IOException {
    578       BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
    579 
    580       sink.writeUtf8(url);
    581       sink.writeByte('\n');
    582       sink.writeUtf8(requestMethod);
    583       sink.writeByte('\n');
    584       sink.writeDecimalLong(varyHeaders.size());
    585       sink.writeByte('\n');
    586       for (int i = 0, size = varyHeaders.size(); i < size; i++) {
    587         sink.writeUtf8(varyHeaders.name(i));
    588         sink.writeUtf8(": ");
    589         sink.writeUtf8(varyHeaders.value(i));
    590         sink.writeByte('\n');
    591       }
    592 
    593       sink.writeUtf8(new StatusLine(protocol, code, message).toString());
    594       sink.writeByte('\n');
    595       sink.writeDecimalLong(responseHeaders.size());
    596       sink.writeByte('\n');
    597       for (int i = 0, size = responseHeaders.size(); i < size; i++) {
    598         sink.writeUtf8(responseHeaders.name(i));
    599         sink.writeUtf8(": ");
    600         sink.writeUtf8(responseHeaders.value(i));
    601         sink.writeByte('\n');
    602       }
    603 
    604       if (isHttps()) {
    605         sink.writeByte('\n');
    606         sink.writeUtf8(handshake.cipherSuite());
    607         sink.writeByte('\n');
    608         writeCertList(sink, handshake.peerCertificates());
    609         writeCertList(sink, handshake.localCertificates());
    610       }
    611       sink.close();
    612     }
    613 
    614     private boolean isHttps() {
    615       return url.startsWith("https://");
    616     }
    617 
    618     private List<Certificate> readCertificateList(BufferedSource source) throws IOException {
    619       int length = readInt(source);
    620       if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null.
    621 
    622       try {
    623         CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    624         List<Certificate> result = new ArrayList<>(length);
    625         for (int i = 0; i < length; i++) {
    626           String line = source.readUtf8LineStrict();
    627           Buffer bytes = new Buffer();
    628           bytes.write(ByteString.decodeBase64(line));
    629           result.add(certificateFactory.generateCertificate(bytes.inputStream()));
    630         }
    631         return result;
    632       } catch (CertificateException e) {
    633         throw new IOException(e.getMessage());
    634       }
    635     }
    636 
    637     private void writeCertList(BufferedSink sink, List<Certificate> certificates)
    638         throws IOException {
    639       try {
    640         sink.writeDecimalLong(certificates.size());
    641         sink.writeByte('\n');
    642         for (int i = 0, size = certificates.size(); i < size; i++) {
    643           byte[] bytes = certificates.get(i).getEncoded();
    644           String line = ByteString.of(bytes).base64();
    645           sink.writeUtf8(line);
    646           sink.writeByte('\n');
    647         }
    648       } catch (CertificateEncodingException e) {
    649         throw new IOException(e.getMessage());
    650       }
    651     }
    652 
    653     public boolean matches(Request request, Response response) {
    654       return url.equals(request.urlString())
    655           && requestMethod.equals(request.method())
    656           && OkHeaders.varyMatches(response, varyHeaders, request);
    657     }
    658 
    659     public Response response(Request request, DiskLruCache.Snapshot snapshot) {
    660       String contentType = responseHeaders.get("Content-Type");
    661       String contentLength = responseHeaders.get("Content-Length");
    662       Request cacheRequest = new Request.Builder()
    663           .url(url)
    664           .method(requestMethod, null)
    665           .headers(varyHeaders)
    666           .build();
    667       return new Response.Builder()
    668           .request(cacheRequest)
    669           .protocol(protocol)
    670           .code(code)
    671           .message(message)
    672           .headers(responseHeaders)
    673           .body(new CacheResponseBody(snapshot, contentType, contentLength))
    674           .handshake(handshake)
    675           .build();
    676     }
    677   }
    678 
    679   private static int readInt(BufferedSource source) throws IOException {
    680     try {
    681       long result = source.readDecimalLong();
    682       String line = source.readUtf8LineStrict();
    683       if (result < 0 || result > Integer.MAX_VALUE || !line.isEmpty()) {
    684         throw new IOException("expected an int but was \"" + result + line + "\"");
    685       }
    686       return (int) result;
    687     } catch (NumberFormatException e) {
    688       throw new IOException(e.getMessage());
    689     }
    690   }
    691 
    692   private static class CacheResponseBody extends ResponseBody {
    693     private final DiskLruCache.Snapshot snapshot;
    694     private final BufferedSource bodySource;
    695     private final String contentType;
    696     private final String contentLength;
    697 
    698     public CacheResponseBody(final DiskLruCache.Snapshot snapshot,
    699         String contentType, String contentLength) {
    700       this.snapshot = snapshot;
    701       this.contentType = contentType;
    702       this.contentLength = contentLength;
    703 
    704       Source source = snapshot.getSource(ENTRY_BODY);
    705       bodySource = Okio.buffer(new ForwardingSource(source) {
    706         @Override public void close() throws IOException {
    707           snapshot.close();
    708           super.close();
    709         }
    710       });
    711     }
    712 
    713     @Override public MediaType contentType() {
    714       return contentType != null ? MediaType.parse(contentType) : null;
    715     }
    716 
    717     @Override public long contentLength() {
    718       try {
    719         return contentLength != null ? Long.parseLong(contentLength) : -1;
    720       } catch (NumberFormatException e) {
    721         return -1;
    722       }
    723     }
    724 
    725     @Override public BufferedSource source() {
    726       return bodySource;
    727     }
    728   }
    729 }
    730