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