Home | History | Annotate | Download | only in internal
      1 /*
      2  * Copyright (C) 2011 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.internal;
     18 
     19 import java.io.BufferedWriter;
     20 import java.io.Closeable;
     21 import java.io.EOFException;
     22 import java.io.File;
     23 import java.io.FileInputStream;
     24 import java.io.FileNotFoundException;
     25 import java.io.FileOutputStream;
     26 import java.io.FileWriter;
     27 import java.io.FilterOutputStream;
     28 import java.io.IOException;
     29 import java.io.InputStream;
     30 import java.io.InputStreamReader;
     31 import java.io.OutputStream;
     32 import java.io.OutputStreamWriter;
     33 import java.io.Writer;
     34 import java.util.ArrayList;
     35 import java.util.Arrays;
     36 import java.util.Iterator;
     37 import java.util.LinkedHashMap;
     38 import java.util.Map;
     39 import java.util.concurrent.Callable;
     40 import java.util.concurrent.ExecutorService;
     41 import java.util.concurrent.LinkedBlockingQueue;
     42 import java.util.concurrent.ThreadPoolExecutor;
     43 import java.util.concurrent.TimeUnit;
     44 
     45 import static com.squareup.okhttp.internal.Util.UTF_8;
     46 
     47 /**
     48  * A cache that uses a bounded amount of space on a filesystem. Each cache
     49  * entry has a string key and a fixed number of values. Values are byte
     50  * sequences, accessible as streams or files. Each value must be between {@code
     51  * 0} and {@code Integer.MAX_VALUE} bytes in length.
     52  *
     53  * <p>The cache stores its data in a directory on the filesystem. This
     54  * directory must be exclusive to the cache; the cache may delete or overwrite
     55  * files from its directory. It is an error for multiple processes to use the
     56  * same cache directory at the same time.
     57  *
     58  * <p>This cache limits the number of bytes that it will store on the
     59  * filesystem. When the number of stored bytes exceeds the limit, the cache will
     60  * remove entries in the background until the limit is satisfied. The limit is
     61  * not strict: the cache may temporarily exceed it while waiting for files to be
     62  * deleted. The limit does not include filesystem overhead or the cache
     63  * journal so space-sensitive applications should set a conservative limit.
     64  *
     65  * <p>Clients call {@link #edit} to create or update the values of an entry. An
     66  * entry may have only one editor at one time; if a value is not available to be
     67  * edited then {@link #edit} will return null.
     68  * <ul>
     69  * <li>When an entry is being <strong>created</strong> it is necessary to
     70  * supply a full set of values; the empty value should be used as a
     71  * placeholder if necessary.
     72  * <li>When an entry is being <strong>edited</strong>, it is not necessary
     73  * to supply data for every value; values default to their previous
     74  * value.
     75  * </ul>
     76  * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
     77  * or {@link Editor#abort}. Committing is atomic: a read observes the full set
     78  * of values as they were before or after the commit, but never a mix of values.
     79  *
     80  * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
     81  * observe the value at the time that {@link #get} was called. Updates and
     82  * removals after the call do not impact ongoing reads.
     83  *
     84  * <p>This class is tolerant of some I/O errors. If files are missing from the
     85  * filesystem, the corresponding entries will be dropped from the cache. If
     86  * an error occurs while writing a cache value, the edit will fail silently.
     87  * Callers should handle other problems by catching {@code IOException} and
     88  * responding appropriately.
     89  */
     90 public final class DiskLruCache implements Closeable {
     91   static final String JOURNAL_FILE = "journal";
     92   static final String JOURNAL_FILE_TMP = "journal.tmp";
     93   static final String MAGIC = "libcore.io.DiskLruCache";
     94   static final String VERSION_1 = "1";
     95   static final long ANY_SEQUENCE_NUMBER = -1;
     96   private static final String CLEAN = "CLEAN";
     97   private static final String DIRTY = "DIRTY";
     98   private static final String REMOVE = "REMOVE";
     99   private static final String READ = "READ";
    100 
    101   // This cache uses a journal file named "journal". A typical journal file
    102   // looks like this:
    103   //     libcore.io.DiskLruCache
    104   //     1
    105   //     100
    106   //     2
    107   //
    108   //     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
    109   //     DIRTY 335c4c6028171cfddfbaae1a9c313c52
    110   //     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
    111   //     REMOVE 335c4c6028171cfddfbaae1a9c313c52
    112   //     DIRTY 1ab96a171faeeee38496d8b330771a7a
    113   //     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
    114   //     READ 335c4c6028171cfddfbaae1a9c313c52
    115   //     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
    116   //
    117   // The first five lines of the journal form its header. They are the
    118   // constant string "libcore.io.DiskLruCache", the disk cache's version,
    119   // the application's version, the value count, and a blank line.
    120   //
    121   // Each of the subsequent lines in the file is a record of the state of a
    122   // cache entry. Each line contains space-separated values: a state, a key,
    123   // and optional state-specific values.
    124   //   o DIRTY lines track that an entry is actively being created or updated.
    125   //     Every successful DIRTY action should be followed by a CLEAN or REMOVE
    126   //     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
    127   //     temporary files may need to be deleted.
    128   //   o CLEAN lines track a cache entry that has been successfully published
    129   //     and may be read. A publish line is followed by the lengths of each of
    130   //     its values.
    131   //   o READ lines track accesses for LRU.
    132   //   o REMOVE lines track entries that have been deleted.
    133   //
    134   // The journal file is appended to as cache operations occur. The journal may
    135   // occasionally be compacted by dropping redundant lines. A temporary file named
    136   // "journal.tmp" will be used during compaction; that file should be deleted if
    137   // it exists when the cache is opened.
    138 
    139   private final File directory;
    140   private final File journalFile;
    141   private final File journalFileTmp;
    142   private final int appVersion;
    143   private final long maxSize;
    144   private final int valueCount;
    145   private long size = 0;
    146   private Writer journalWriter;
    147   private final LinkedHashMap<String, Entry> lruEntries =
    148       new LinkedHashMap<String, Entry>(0, 0.75f, true);
    149   private int redundantOpCount;
    150 
    151   /**
    152    * To differentiate between old and current snapshots, each entry is given
    153    * a sequence number each time an edit is committed. A snapshot is stale if
    154    * its sequence number is not equal to its entry's sequence number.
    155    */
    156   private long nextSequenceNumber = 0;
    157 
    158   /** This cache uses a single background thread to evict entries. */
    159   private final ExecutorService executorService =
    160       new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    161   private final Callable<Void> cleanupCallable = new Callable<Void>() {
    162     @Override public Void call() throws Exception {
    163       synchronized (DiskLruCache.this) {
    164         if (journalWriter == null) {
    165           return null; // closed
    166         }
    167         trimToSize();
    168         if (journalRebuildRequired()) {
    169           rebuildJournal();
    170           redundantOpCount = 0;
    171         }
    172       }
    173       return null;
    174     }
    175   };
    176 
    177   private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
    178     this.directory = directory;
    179     this.appVersion = appVersion;
    180     this.journalFile = new File(directory, JOURNAL_FILE);
    181     this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
    182     this.valueCount = valueCount;
    183     this.maxSize = maxSize;
    184   }
    185 
    186   /**
    187    * Opens the cache in {@code directory}, creating a cache if none exists
    188    * there.
    189    *
    190    * @param directory a writable directory
    191    * @param valueCount the number of values per cache entry. Must be positive.
    192    * @param maxSize the maximum number of bytes this cache should use to store
    193    * @throws IOException if reading or writing the cache directory fails
    194    */
    195   public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
    196       throws IOException {
    197     if (maxSize <= 0) {
    198       throw new IllegalArgumentException("maxSize <= 0");
    199     }
    200     if (valueCount <= 0) {
    201       throw new IllegalArgumentException("valueCount <= 0");
    202     }
    203 
    204     // prefer to pick up where we left off
    205     DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    206     if (cache.journalFile.exists()) {
    207       try {
    208         cache.readJournal();
    209         cache.processJournal();
    210         cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));
    211         return cache;
    212       } catch (IOException journalIsCorrupt) {
    213         Platform.get()
    214             .logW("DiskLruCache "
    215                 + directory
    216                 + " is corrupt: "
    217                 + journalIsCorrupt.getMessage()
    218                 + ", removing");
    219         cache.delete();
    220       }
    221     }
    222 
    223     // create a new empty cache
    224     directory.mkdirs();
    225     cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    226     cache.rebuildJournal();
    227     return cache;
    228   }
    229 
    230   private void readJournal() throws IOException {
    231     StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
    232     try {
    233       String magic = reader.readLine();
    234       String version = reader.readLine();
    235       String appVersionString = reader.readLine();
    236       String valueCountString = reader.readLine();
    237       String blank = reader.readLine();
    238       if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion)
    239           .equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !""
    240           .equals(blank)) {
    241         throw new IOException("unexpected journal header: ["
    242             + magic
    243             + ", "
    244             + version
    245             + ", "
    246             + valueCountString
    247             + ", "
    248             + blank
    249             + "]");
    250       }
    251 
    252       while (true) {
    253         try {
    254           readJournalLine(reader.readLine());
    255         } catch (EOFException endOfJournal) {
    256           break;
    257         }
    258       }
    259     } finally {
    260       Util.closeQuietly(reader);
    261     }
    262   }
    263 
    264   private void readJournalLine(String line) throws IOException {
    265     String[] parts = line.split(" ");
    266     if (parts.length < 2) {
    267       throw new IOException("unexpected journal line: " + line);
    268     }
    269 
    270     String key = parts[1];
    271     if (parts[0].equals(REMOVE) && parts.length == 2) {
    272       lruEntries.remove(key);
    273       return;
    274     }
    275 
    276     Entry entry = lruEntries.get(key);
    277     if (entry == null) {
    278       entry = new Entry(key);
    279       lruEntries.put(key, entry);
    280     }
    281 
    282     if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
    283       entry.readable = true;
    284       entry.currentEditor = null;
    285       entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length));
    286     } else if (parts[0].equals(DIRTY) && parts.length == 2) {
    287       entry.currentEditor = new Editor(entry);
    288     } else if (parts[0].equals(READ) && parts.length == 2) {
    289       // this work was already done by calling lruEntries.get()
    290     } else {
    291       throw new IOException("unexpected journal line: " + line);
    292     }
    293   }
    294 
    295   /**
    296    * Computes the initial size and collects garbage as a part of opening the
    297    * cache. Dirty entries are assumed to be inconsistent and will be deleted.
    298    */
    299   private void processJournal() throws IOException {
    300     deleteIfExists(journalFileTmp);
    301     for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
    302       Entry entry = i.next();
    303       if (entry.currentEditor == null) {
    304         for (int t = 0; t < valueCount; t++) {
    305           size += entry.lengths[t];
    306         }
    307       } else {
    308         entry.currentEditor = null;
    309         for (int t = 0; t < valueCount; t++) {
    310           deleteIfExists(entry.getCleanFile(t));
    311           deleteIfExists(entry.getDirtyFile(t));
    312         }
    313         i.remove();
    314       }
    315     }
    316   }
    317 
    318   /**
    319    * Creates a new journal that omits redundant information. This replaces the
    320    * current journal if it exists.
    321    */
    322   private synchronized void rebuildJournal() throws IOException {
    323     if (journalWriter != null) {
    324       journalWriter.close();
    325     }
    326 
    327     Writer writer = new BufferedWriter(new FileWriter(journalFileTmp));
    328     writer.write(MAGIC);
    329     writer.write("\n");
    330     writer.write(VERSION_1);
    331     writer.write("\n");
    332     writer.write(Integer.toString(appVersion));
    333     writer.write("\n");
    334     writer.write(Integer.toString(valueCount));
    335     writer.write("\n");
    336     writer.write("\n");
    337 
    338     for (Entry entry : lruEntries.values()) {
    339       if (entry.currentEditor != null) {
    340         writer.write(DIRTY + ' ' + entry.key + '\n');
    341       } else {
    342         writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
    343       }
    344     }
    345 
    346     writer.close();
    347     journalFileTmp.renameTo(journalFile);
    348     journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
    349   }
    350 
    351   private static void deleteIfExists(File file) throws IOException {
    352     file.delete();
    353   }
    354 
    355   /**
    356    * Returns a snapshot of the entry named {@code key}, or null if it doesn't
    357    * exist is not currently readable. If a value is returned, it is moved to
    358    * the head of the LRU queue.
    359    */
    360   public synchronized Snapshot get(String key) throws IOException {
    361     checkNotClosed();
    362     validateKey(key);
    363     Entry entry = lruEntries.get(key);
    364     if (entry == null) {
    365       return null;
    366     }
    367 
    368     if (!entry.readable) {
    369       return null;
    370     }
    371 
    372     // Open all streams eagerly to guarantee that we see a single published
    373     // snapshot. If we opened streams lazily then the streams could come
    374     // from different edits.
    375     InputStream[] ins = new InputStream[valueCount];
    376     try {
    377       for (int i = 0; i < valueCount; i++) {
    378         ins[i] = new FileInputStream(entry.getCleanFile(i));
    379       }
    380     } catch (FileNotFoundException e) {
    381       // a file must have been deleted manually!
    382       return null;
    383     }
    384 
    385     redundantOpCount++;
    386     journalWriter.append(READ + ' ' + key + '\n');
    387     if (journalRebuildRequired()) {
    388       executorService.submit(cleanupCallable);
    389     }
    390 
    391     return new Snapshot(key, entry.sequenceNumber, ins);
    392   }
    393 
    394   /**
    395    * Returns an editor for the entry named {@code key}, or null if another
    396    * edit is in progress.
    397    */
    398   public Editor edit(String key) throws IOException {
    399     return edit(key, ANY_SEQUENCE_NUMBER);
    400   }
    401 
    402   private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    403     checkNotClosed();
    404     validateKey(key);
    405     Entry entry = lruEntries.get(key);
    406     if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
    407         || entry.sequenceNumber != expectedSequenceNumber)) {
    408       return null; // snapshot is stale
    409     }
    410     if (entry == null) {
    411       entry = new Entry(key);
    412       lruEntries.put(key, entry);
    413     } else if (entry.currentEditor != null) {
    414       return null; // another edit is in progress
    415     }
    416 
    417     Editor editor = new Editor(entry);
    418     entry.currentEditor = editor;
    419 
    420     // flush the journal before creating files to prevent file leaks
    421     journalWriter.write(DIRTY + ' ' + key + '\n');
    422     journalWriter.flush();
    423     return editor;
    424   }
    425 
    426   /** Returns the directory where this cache stores its data. */
    427   public File getDirectory() {
    428     return directory;
    429   }
    430 
    431   /**
    432    * Returns the maximum number of bytes that this cache should use to store
    433    * its data.
    434    */
    435   public long maxSize() {
    436     return maxSize;
    437   }
    438 
    439   /**
    440    * Returns the number of bytes currently being used to store the values in
    441    * this cache. This may be greater than the max size if a background
    442    * deletion is pending.
    443    */
    444   public synchronized long size() {
    445     return size;
    446   }
    447 
    448   private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    449     Entry entry = editor.entry;
    450     if (entry.currentEditor != editor) {
    451       throw new IllegalStateException();
    452     }
    453 
    454     // if this edit is creating the entry for the first time, every index must have a value
    455     if (success && !entry.readable) {
    456       for (int i = 0; i < valueCount; i++) {
    457         if (!editor.written[i]) {
    458           editor.abort();
    459           throw new IllegalStateException("Newly created entry didn't create value for index " + i);
    460         }
    461         if (!entry.getDirtyFile(i).exists()) {
    462           editor.abort();
    463           Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i);
    464           return;
    465         }
    466       }
    467     }
    468 
    469     for (int i = 0; i < valueCount; i++) {
    470       File dirty = entry.getDirtyFile(i);
    471       if (success) {
    472         if (dirty.exists()) {
    473           File clean = entry.getCleanFile(i);
    474           dirty.renameTo(clean);
    475           long oldLength = entry.lengths[i];
    476           long newLength = clean.length();
    477           entry.lengths[i] = newLength;
    478           size = size - oldLength + newLength;
    479         }
    480       } else {
    481         deleteIfExists(dirty);
    482       }
    483     }
    484 
    485     redundantOpCount++;
    486     entry.currentEditor = null;
    487     if (entry.readable | success) {
    488       entry.readable = true;
    489       journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
    490       if (success) {
    491         entry.sequenceNumber = nextSequenceNumber++;
    492       }
    493     } else {
    494       lruEntries.remove(entry.key);
    495       journalWriter.write(REMOVE + ' ' + entry.key + '\n');
    496     }
    497 
    498     if (size > maxSize || journalRebuildRequired()) {
    499       executorService.submit(cleanupCallable);
    500     }
    501   }
    502 
    503   /**
    504    * We only rebuild the journal when it will halve the size of the journal
    505    * and eliminate at least 2000 ops.
    506    */
    507   private boolean journalRebuildRequired() {
    508     final int redundantOpCompactThreshold = 2000;
    509     return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();
    510   }
    511 
    512   /**
    513    * Drops the entry for {@code key} if it exists and can be removed. Entries
    514    * actively being edited cannot be removed.
    515    *
    516    * @return true if an entry was removed.
    517    */
    518   public synchronized boolean remove(String key) throws IOException {
    519     checkNotClosed();
    520     validateKey(key);
    521     Entry entry = lruEntries.get(key);
    522     if (entry == null || entry.currentEditor != null) {
    523       return false;
    524     }
    525 
    526     for (int i = 0; i < valueCount; i++) {
    527       File file = entry.getCleanFile(i);
    528       if (!file.delete()) {
    529         throw new IOException("failed to delete " + file);
    530       }
    531       size -= entry.lengths[i];
    532       entry.lengths[i] = 0;
    533     }
    534 
    535     redundantOpCount++;
    536     journalWriter.append(REMOVE + ' ' + key + '\n');
    537     lruEntries.remove(key);
    538 
    539     if (journalRebuildRequired()) {
    540       executorService.submit(cleanupCallable);
    541     }
    542 
    543     return true;
    544   }
    545 
    546   /** Returns true if this cache has been closed. */
    547   public boolean isClosed() {
    548     return journalWriter == null;
    549   }
    550 
    551   private void checkNotClosed() {
    552     if (journalWriter == null) {
    553       throw new IllegalStateException("cache is closed");
    554     }
    555   }
    556 
    557   /** Force buffered operations to the filesystem. */
    558   public synchronized void flush() throws IOException {
    559     checkNotClosed();
    560     trimToSize();
    561     journalWriter.flush();
    562   }
    563 
    564   /** Closes this cache. Stored values will remain on the filesystem. */
    565   public synchronized void close() throws IOException {
    566     if (journalWriter == null) {
    567       return; // already closed
    568     }
    569     for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
    570       if (entry.currentEditor != null) {
    571         entry.currentEditor.abort();
    572       }
    573     }
    574     trimToSize();
    575     journalWriter.close();
    576     journalWriter = null;
    577   }
    578 
    579   private void trimToSize() throws IOException {
    580     while (size > maxSize) {
    581       Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
    582       remove(toEvict.getKey());
    583     }
    584   }
    585 
    586   /**
    587    * Closes the cache and deletes all of its stored values. This will delete
    588    * all files in the cache directory including files that weren't created by
    589    * the cache.
    590    */
    591   public void delete() throws IOException {
    592     close();
    593     Util.deleteContents(directory);
    594   }
    595 
    596   private void validateKey(String key) {
    597     if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
    598       throw new IllegalArgumentException(
    599           "keys must not contain spaces or newlines: \"" + key + "\"");
    600     }
    601   }
    602 
    603   private static String inputStreamToString(InputStream in) throws IOException {
    604     return Util.readFully(new InputStreamReader(in, UTF_8));
    605   }
    606 
    607   /** A snapshot of the values for an entry. */
    608   public final class Snapshot implements Closeable {
    609     private final String key;
    610     private final long sequenceNumber;
    611     private final InputStream[] ins;
    612 
    613     private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
    614       this.key = key;
    615       this.sequenceNumber = sequenceNumber;
    616       this.ins = ins;
    617     }
    618 
    619     /**
    620      * Returns an editor for this snapshot's entry, or null if either the
    621      * entry has changed since this snapshot was created or if another edit
    622      * is in progress.
    623      */
    624     public Editor edit() throws IOException {
    625       return DiskLruCache.this.edit(key, sequenceNumber);
    626     }
    627 
    628     /** Returns the unbuffered stream with the value for {@code index}. */
    629     public InputStream getInputStream(int index) {
    630       return ins[index];
    631     }
    632 
    633     /** Returns the string value for {@code index}. */
    634     public String getString(int index) throws IOException {
    635       return inputStreamToString(getInputStream(index));
    636     }
    637 
    638     @Override public void close() {
    639       for (InputStream in : ins) {
    640         Util.closeQuietly(in);
    641       }
    642     }
    643   }
    644 
    645   /** Edits the values for an entry. */
    646   public final class Editor {
    647     private final Entry entry;
    648     private final boolean[] written;
    649     private boolean hasErrors;
    650 
    651     private Editor(Entry entry) {
    652       this.entry = entry;
    653       this.written = (entry.readable) ? null : new boolean[valueCount];
    654     }
    655 
    656     /**
    657      * Returns an unbuffered input stream to read the last committed value,
    658      * or null if no value has been committed.
    659      */
    660     public InputStream newInputStream(int index) throws IOException {
    661       synchronized (DiskLruCache.this) {
    662         if (entry.currentEditor != this) {
    663           throw new IllegalStateException();
    664         }
    665         if (!entry.readable) {
    666           return null;
    667         }
    668         return new FileInputStream(entry.getCleanFile(index));
    669       }
    670     }
    671 
    672     /**
    673      * Returns the last committed value as a string, or null if no value
    674      * has been committed.
    675      */
    676     public String getString(int index) throws IOException {
    677       InputStream in = newInputStream(index);
    678       return in != null ? inputStreamToString(in) : null;
    679     }
    680 
    681     /**
    682      * Returns a new unbuffered output stream to write the value at
    683      * {@code index}. If the underlying output stream encounters errors
    684      * when writing to the filesystem, this edit will be aborted when
    685      * {@link #commit} is called. The returned output stream does not throw
    686      * IOExceptions.
    687      */
    688     public OutputStream newOutputStream(int index) throws IOException {
    689       synchronized (DiskLruCache.this) {
    690         if (entry.currentEditor != this) {
    691           throw new IllegalStateException();
    692         }
    693         if (!entry.readable) {
    694           written[index] = true;
    695         }
    696         return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
    697       }
    698     }
    699 
    700     /** Sets the value at {@code index} to {@code value}. */
    701     public void set(int index, String value) throws IOException {
    702       Writer writer = null;
    703       try {
    704         writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
    705         writer.write(value);
    706       } finally {
    707         Util.closeQuietly(writer);
    708       }
    709     }
    710 
    711     /**
    712      * Commits this edit so it is visible to readers.  This releases the
    713      * edit lock so another edit may be started on the same key.
    714      */
    715     public void commit() throws IOException {
    716       if (hasErrors) {
    717         completeEdit(this, false);
    718         remove(entry.key); // the previous entry is stale
    719       } else {
    720         completeEdit(this, true);
    721       }
    722     }
    723 
    724     /**
    725      * Aborts this edit. This releases the edit lock so another edit may be
    726      * started on the same key.
    727      */
    728     public void abort() throws IOException {
    729       completeEdit(this, false);
    730     }
    731 
    732     private final class FaultHidingOutputStream extends FilterOutputStream {
    733       private FaultHidingOutputStream(OutputStream out) {
    734         super(out);
    735       }
    736 
    737       @Override public void write(int oneByte) {
    738         try {
    739           out.write(oneByte);
    740         } catch (IOException e) {
    741           hasErrors = true;
    742         }
    743       }
    744 
    745       @Override public void write(byte[] buffer, int offset, int length) {
    746         try {
    747           out.write(buffer, offset, length);
    748         } catch (IOException e) {
    749           hasErrors = true;
    750         }
    751       }
    752 
    753       @Override public void close() {
    754         try {
    755           out.close();
    756         } catch (IOException e) {
    757           hasErrors = true;
    758         }
    759       }
    760 
    761       @Override public void flush() {
    762         try {
    763           out.flush();
    764         } catch (IOException e) {
    765           hasErrors = true;
    766         }
    767       }
    768     }
    769   }
    770 
    771   private final class Entry {
    772     private final String key;
    773 
    774     /** Lengths of this entry's files. */
    775     private final long[] lengths;
    776 
    777     /** True if this entry has ever been published. */
    778     private boolean readable;
    779 
    780     /** The ongoing edit or null if this entry is not being edited. */
    781     private Editor currentEditor;
    782 
    783     /** The sequence number of the most recently committed edit to this entry. */
    784     private long sequenceNumber;
    785 
    786     private Entry(String key) {
    787       this.key = key;
    788       this.lengths = new long[valueCount];
    789     }
    790 
    791     public String getLengths() throws IOException {
    792       StringBuilder result = new StringBuilder();
    793       for (long size : lengths) {
    794         result.append(' ').append(size);
    795       }
    796       return result.toString();
    797     }
    798 
    799     /** Set lengths using decimal numbers like "10123". */
    800     private void setLengths(String[] strings) throws IOException {
    801       if (strings.length != valueCount) {
    802         throw invalidLengths(strings);
    803       }
    804 
    805       try {
    806         for (int i = 0; i < strings.length; i++) {
    807           lengths[i] = Long.parseLong(strings[i]);
    808         }
    809       } catch (NumberFormatException e) {
    810         throw invalidLengths(strings);
    811       }
    812     }
    813 
    814     private IOException invalidLengths(String[] strings) throws IOException {
    815       throw new IOException("unexpected journal line: " + Arrays.toString(strings));
    816     }
    817 
    818     public File getCleanFile(int i) {
    819       return new File(directory, key + "." + i);
    820     }
    821 
    822     public File getDirtyFile(int i) {
    823       return new File(directory, key + "." + i + ".tmp");
    824     }
    825   }
    826 }
    827