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.Closeable;
20import java.io.EOFException;
21import java.io.File;
22import java.io.FileInputStream;
23import java.io.FileNotFoundException;
24import java.io.FileOutputStream;
25import java.io.FilterOutputStream;
26import java.io.IOException;
27import java.io.InputStream;
28import java.io.OutputStream;
29import java.util.Iterator;
30import java.util.LinkedHashMap;
31import java.util.Map;
32import java.util.concurrent.LinkedBlockingQueue;
33import java.util.concurrent.ThreadPoolExecutor;
34import java.util.concurrent.TimeUnit;
35import java.util.regex.Matcher;
36import java.util.regex.Pattern;
37import okio.BufferedSink;
38import okio.BufferedSource;
39import okio.OkBuffer;
40import okio.Okio;
41
42/**
43 * A cache that uses a bounded amount of space on a filesystem. Each cache
44 * entry has a string key and a fixed number of values. Each key must match
45 * the regex <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences,
46 * accessible as streams or files. Each value must be between {@code 0} and
47 * {@code Integer.MAX_VALUE} bytes in length.
48 *
49 * <p>The cache stores its data in a directory on the filesystem. This
50 * directory must be exclusive to the cache; the cache may delete or overwrite
51 * files from its directory. It is an error for multiple processes to use the
52 * same cache directory at the same time.
53 *
54 * <p>This cache limits the number of bytes that it will store on the
55 * filesystem. When the number of stored bytes exceeds the limit, the cache will
56 * remove entries in the background until the limit is satisfied. The limit is
57 * not strict: the cache may temporarily exceed it while waiting for files to be
58 * deleted. The limit does not include filesystem overhead or the cache
59 * journal so space-sensitive applications should set a conservative limit.
60 *
61 * <p>Clients call {@link #edit} to create or update the values of an entry. An
62 * entry may have only one editor at one time; if a value is not available to be
63 * edited then {@link #edit} will return null.
64 * <ul>
65 *     <li>When an entry is being <strong>created</strong> it is necessary to
66 *         supply a full set of values; the empty value should be used as a
67 *         placeholder if necessary.
68 *     <li>When an entry is being <strong>edited</strong>, it is not necessary
69 *         to supply data for every value; values default to their previous
70 *         value.
71 * </ul>
72 * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
73 * or {@link Editor#abort}. Committing is atomic: a read observes the full set
74 * of values as they were before or after the commit, but never a mix of values.
75 *
76 * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
77 * observe the value at the time that {@link #get} was called. Updates and
78 * removals after the call do not impact ongoing reads.
79 *
80 * <p>This class is tolerant of some I/O errors. If files are missing from the
81 * filesystem, the corresponding entries will be dropped from the cache. If
82 * an error occurs while writing a cache value, the edit will fail silently.
83 * Callers should handle other problems by catching {@code IOException} and
84 * responding appropriately.
85 */
86public final class DiskLruCache implements Closeable {
87  static final String JOURNAL_FILE = "journal";
88  static final String JOURNAL_FILE_TEMP = "journal.tmp";
89  static final String JOURNAL_FILE_BACKUP = "journal.bkp";
90  static final String MAGIC = "libcore.io.DiskLruCache";
91  static final String VERSION_1 = "1";
92  static final long ANY_SEQUENCE_NUMBER = -1;
93  static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
94  private static final String CLEAN = "CLEAN";
95  private static final String DIRTY = "DIRTY";
96  private static final String REMOVE = "REMOVE";
97  private static final String READ = "READ";
98
99    /*
100     * This cache uses a journal file named "journal". A typical journal file
101     * looks like this:
102     *     libcore.io.DiskLruCache
103     *     1
104     *     100
105     *     2
106     *
107     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
108     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
109     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
110     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
111     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
112     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
113     *     READ 335c4c6028171cfddfbaae1a9c313c52
114     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
115     *
116     * The first five lines of the journal form its header. They are the
117     * constant string "libcore.io.DiskLruCache", the disk cache's version,
118     * the application's version, the value count, and a blank line.
119     *
120     * Each of the subsequent lines in the file is a record of the state of a
121     * cache entry. Each line contains space-separated values: a state, a key,
122     * and optional state-specific values.
123     *   o DIRTY lines track that an entry is actively being created or updated.
124     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
125     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
126     *     temporary files may need to be deleted.
127     *   o CLEAN lines track a cache entry that has been successfully published
128     *     and may be read. A publish line is followed by the lengths of each of
129     *     its values.
130     *   o READ lines track accesses for LRU.
131     *   o REMOVE lines track entries that have been deleted.
132     *
133     * The journal file is appended to as cache operations occur. The journal may
134     * occasionally be compacted by dropping redundant lines. A temporary file named
135     * "journal.tmp" will be used during compaction; that file should be deleted if
136     * it exists when the cache is opened.
137     */
138
139  private final File directory;
140  private final File journalFile;
141  private final File journalFileTmp;
142  private final File journalFileBackup;
143  private final int appVersion;
144  private long maxSize;
145  private final int valueCount;
146  private long size = 0;
147  private BufferedSink journalWriter;
148  private final LinkedHashMap<String, Entry> lruEntries =
149      new LinkedHashMap<String, Entry>(0, 0.75f, true);
150  private int redundantOpCount;
151
152  /**
153   * To differentiate between old and current snapshots, each entry is given
154   * a sequence number each time an edit is committed. A snapshot is stale if
155   * its sequence number is not equal to its entry's sequence number.
156   */
157  private long nextSequenceNumber = 0;
158
159  /** This cache uses a single background thread to evict entries. */
160  final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
161      new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
162  private final Runnable cleanupRunnable = new Runnable() {
163    public void run() {
164      synchronized (DiskLruCache.this) {
165        if (journalWriter == null) {
166          return; // Closed.
167        }
168        try {
169          trimToSize();
170          if (journalRebuildRequired()) {
171            rebuildJournal();
172            redundantOpCount = 0;
173          }
174        } catch (IOException e) {
175          throw new RuntimeException(e);
176        }
177      }
178    }
179  };
180
181  private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
182    this.directory = directory;
183    this.appVersion = appVersion;
184    this.journalFile = new File(directory, JOURNAL_FILE);
185    this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
186    this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
187    this.valueCount = valueCount;
188    this.maxSize = maxSize;
189  }
190
191  /**
192   * Opens the cache in {@code directory}, creating a cache if none exists
193   * there.
194   *
195   * @param directory a writable directory
196   * @param valueCount the number of values per cache entry. Must be positive.
197   * @param maxSize the maximum number of bytes this cache should use to store
198   * @throws IOException if reading or writing the cache directory fails
199   */
200  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
201      throws IOException {
202    if (maxSize <= 0) {
203      throw new IllegalArgumentException("maxSize <= 0");
204    }
205    if (valueCount <= 0) {
206      throw new IllegalArgumentException("valueCount <= 0");
207    }
208
209    // If a bkp file exists, use it instead.
210    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
211    if (backupFile.exists()) {
212      File journalFile = new File(directory, JOURNAL_FILE);
213      // If journal file also exists just delete backup file.
214      if (journalFile.exists()) {
215        backupFile.delete();
216      } else {
217        renameTo(backupFile, journalFile, false);
218      }
219    }
220
221    // Prefer to pick up where we left off.
222    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
223    if (cache.journalFile.exists()) {
224      try {
225        cache.readJournal();
226        cache.processJournal();
227        cache.journalWriter = Okio.buffer(Okio.sink(new FileOutputStream(cache.journalFile, true)));
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    BufferedSource source = Okio.buffer(Okio.source(new FileInputStream(journalFile)));
245    try {
246      String magic = source.readUtf8LineStrict();
247      String version = source.readUtf8LineStrict();
248      String appVersionString = source.readUtf8LineStrict();
249      String valueCountString = source.readUtf8LineStrict();
250      String blank = source.readUtf8LineStrict();
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(source.readUtf8LineStrict());
264          lineCount++;
265        } catch (EOFException endOfJournal) {
266          break;
267        }
268      }
269      redundantOpCount = lineCount - lruEntries.size();
270    } finally {
271      Util.closeQuietly(source);
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    BufferedSink writer = Okio.buffer(Okio.sink(new FileOutputStream(journalFileTmp)));
347    try {
348      writer.writeUtf8(MAGIC);
349      writer.writeUtf8("\n");
350      writer.writeUtf8(VERSION_1);
351      writer.writeUtf8("\n");
352      writer.writeUtf8(Integer.toString(appVersion));
353      writer.writeUtf8("\n");
354      writer.writeUtf8(Integer.toString(valueCount));
355      writer.writeUtf8("\n");
356      writer.writeUtf8("\n");
357
358      for (Entry entry : lruEntries.values()) {
359        if (entry.currentEditor != null) {
360          writer.writeUtf8(DIRTY + ' ' + entry.key + '\n');
361        } else {
362          writer.writeUtf8(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
363        }
364      }
365    } finally {
366      writer.close();
367    }
368
369    if (journalFile.exists()) {
370      renameTo(journalFile, journalFileBackup, true);
371    }
372    renameTo(journalFileTmp, journalFile, false);
373    journalFileBackup.delete();
374
375    journalWriter = Okio.buffer(Okio.sink(new FileOutputStream(journalFile, true)));
376  }
377
378  private static void deleteIfExists(File file) throws IOException {
379    // If delete() fails, make sure it's because the file didn't exist!
380    if (!file.delete() && file.exists()) {
381      throw new IOException("failed to delete " + file);
382    }
383  }
384
385  private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
386    if (deleteDestination) {
387      deleteIfExists(to);
388    }
389    if (!from.renameTo(to)) {
390      throw new IOException();
391    }
392  }
393
394  /**
395   * Returns a snapshot of the entry named {@code key}, or null if it doesn't
396   * exist is not currently readable. If a value is returned, it is moved to
397   * the head of the LRU queue.
398   */
399  public synchronized Snapshot get(String key) throws IOException {
400    checkNotClosed();
401    validateKey(key);
402    Entry entry = lruEntries.get(key);
403    if (entry == null) {
404      return null;
405    }
406
407    if (!entry.readable) {
408      return null;
409    }
410
411    // Open all streams eagerly to guarantee that we see a single published
412    // snapshot. If we opened streams lazily then the streams could come
413    // from different edits.
414    InputStream[] ins = new InputStream[valueCount];
415    try {
416      for (int i = 0; i < valueCount; i++) {
417        ins[i] = new FileInputStream(entry.getCleanFile(i));
418      }
419    } catch (FileNotFoundException e) {
420      // A file must have been deleted manually!
421      for (int i = 0; i < valueCount; i++) {
422        if (ins[i] != null) {
423          Util.closeQuietly(ins[i]);
424        } else {
425          break;
426        }
427      }
428      return null;
429    }
430
431    redundantOpCount++;
432    journalWriter.writeUtf8(READ + ' ' + key + '\n');
433    if (journalRebuildRequired()) {
434      executorService.execute(cleanupRunnable);
435    }
436
437    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
438  }
439
440  /**
441   * Returns an editor for the entry named {@code key}, or null if another
442   * edit is in progress.
443   */
444  public Editor edit(String key) throws IOException {
445    return edit(key, ANY_SEQUENCE_NUMBER);
446  }
447
448  private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
449    checkNotClosed();
450    validateKey(key);
451    Entry entry = lruEntries.get(key);
452    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
453        || entry.sequenceNumber != expectedSequenceNumber)) {
454      return null; // Snapshot is stale.
455    }
456    if (entry == null) {
457      entry = new Entry(key);
458      lruEntries.put(key, entry);
459    } else if (entry.currentEditor != null) {
460      return null; // Another edit is in progress.
461    }
462
463    Editor editor = new Editor(entry);
464    entry.currentEditor = editor;
465
466    // Flush the journal before creating files to prevent file leaks.
467    journalWriter.writeUtf8(DIRTY + ' ' + key + '\n');
468    journalWriter.flush();
469    return editor;
470  }
471
472  /** Returns the directory where this cache stores its data. */
473  public File getDirectory() {
474    return directory;
475  }
476
477  /**
478   * Returns the maximum number of bytes that this cache should use to store
479   * its data.
480   */
481  public synchronized long getMaxSize() {
482    return maxSize;
483  }
484
485  /**
486   * Changes the maximum number of bytes the cache can store and queues a job
487   * to trim the existing store, if necessary.
488   */
489  public synchronized void setMaxSize(long maxSize) {
490    this.maxSize = maxSize;
491    executorService.execute(cleanupRunnable);
492  }
493
494  /**
495   * Returns the number of bytes currently being used to store the values in
496   * this cache. This may be greater than the max size if a background
497   * deletion is pending.
498   */
499  public synchronized long size() {
500    return size;
501  }
502
503  private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
504    Entry entry = editor.entry;
505    if (entry.currentEditor != editor) {
506      throw new IllegalStateException();
507    }
508
509    // If this edit is creating the entry for the first time, every index must have a value.
510    if (success && !entry.readable) {
511      for (int i = 0; i < valueCount; i++) {
512        if (!editor.written[i]) {
513          editor.abort();
514          throw new IllegalStateException("Newly created entry didn't create value for index " + i);
515        }
516        if (!entry.getDirtyFile(i).exists()) {
517          editor.abort();
518          return;
519        }
520      }
521    }
522
523    for (int i = 0; i < valueCount; i++) {
524      File dirty = entry.getDirtyFile(i);
525      if (success) {
526        if (dirty.exists()) {
527          File clean = entry.getCleanFile(i);
528          dirty.renameTo(clean);
529          long oldLength = entry.lengths[i];
530          long newLength = clean.length();
531          entry.lengths[i] = newLength;
532          size = size - oldLength + newLength;
533        }
534      } else {
535        deleteIfExists(dirty);
536      }
537    }
538
539    redundantOpCount++;
540    entry.currentEditor = null;
541    if (entry.readable | success) {
542      entry.readable = true;
543      journalWriter.writeUtf8(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
544      if (success) {
545        entry.sequenceNumber = nextSequenceNumber++;
546      }
547    } else {
548      lruEntries.remove(entry.key);
549      journalWriter.writeUtf8(REMOVE + ' ' + entry.key + '\n');
550    }
551    journalWriter.flush();
552
553    if (size > maxSize || journalRebuildRequired()) {
554      executorService.execute(cleanupRunnable);
555    }
556  }
557
558  /**
559   * We only rebuild the journal when it will halve the size of the journal
560   * and eliminate at least 2000 ops.
561   */
562  private boolean journalRebuildRequired() {
563    final int redundantOpCompactThreshold = 2000;
564    return redundantOpCount >= redundantOpCompactThreshold
565        && redundantOpCount >= lruEntries.size();
566  }
567
568  /**
569   * Drops the entry for {@code key} if it exists and can be removed. Entries
570   * actively being edited cannot be removed.
571   *
572   * @return true if an entry was removed.
573   */
574  public synchronized boolean remove(String key) throws IOException {
575    checkNotClosed();
576    validateKey(key);
577    Entry entry = lruEntries.get(key);
578    if (entry == null || entry.currentEditor != null) {
579      return false;
580    }
581
582    for (int i = 0; i < valueCount; i++) {
583      File file = entry.getCleanFile(i);
584      deleteIfExists(file);
585      size -= entry.lengths[i];
586      entry.lengths[i] = 0;
587    }
588
589    redundantOpCount++;
590    journalWriter.writeUtf8(REMOVE + ' ' + key + '\n');
591    lruEntries.remove(key);
592
593    if (journalRebuildRequired()) {
594      executorService.execute(cleanupRunnable);
595    }
596
597    return true;
598  }
599
600  /** Returns true if this cache has been closed. */
601  public boolean isClosed() {
602    return journalWriter == null;
603  }
604
605  private void checkNotClosed() {
606    if (journalWriter == null) {
607      throw new IllegalStateException("cache is closed");
608    }
609  }
610
611  /** Force buffered operations to the filesystem. */
612  public synchronized void flush() throws IOException {
613    checkNotClosed();
614    trimToSize();
615    journalWriter.flush();
616  }
617
618  /** Closes this cache. Stored values will remain on the filesystem. */
619  public synchronized void close() throws IOException {
620    if (journalWriter == null) {
621      return; // Already closed.
622    }
623    // Copying for safe iteration.
624    for (Object next : lruEntries.values().toArray()) {
625      Entry entry = (Entry) next;
626      if (entry.currentEditor != null) {
627        entry.currentEditor.abort();
628      }
629    }
630    trimToSize();
631    journalWriter.close();
632    journalWriter = null;
633  }
634
635  private void trimToSize() throws IOException {
636    while (size > maxSize) {
637      Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
638      remove(toEvict.getKey());
639    }
640  }
641
642  /**
643   * Closes the cache and deletes all of its stored values. This will delete
644   * all files in the cache directory including files that weren't created by
645   * the cache.
646   */
647  public void delete() throws IOException {
648    close();
649    Util.deleteContents(directory);
650  }
651
652  private void validateKey(String key) {
653    Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
654    if (!matcher.matches()) {
655      throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
656    }
657  }
658
659  private static String inputStreamToString(InputStream in) throws IOException {
660    OkBuffer buffer = Util.readFully(Okio.source(in));
661    return buffer.readUtf8(buffer.size());
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      BufferedSink writer = Okio.buffer(Okio.sink(newOutputStream(index)));
793      writer.writeUtf8(value);
794      writer.close();
795    }
796
797    /**
798     * Commits this edit so it is visible to readers.  This releases the
799     * edit lock so another edit may be started on the same key.
800     */
801    public void commit() throws IOException {
802      if (hasErrors) {
803        completeEdit(this, false);
804        remove(entry.key); // The previous entry is stale.
805      } else {
806        completeEdit(this, true);
807      }
808      committed = true;
809    }
810
811    /**
812     * Aborts this edit. This releases the edit lock so another edit may be
813     * started on the same key.
814     */
815    public void abort() throws IOException {
816      completeEdit(this, false);
817    }
818
819    public void abortUnlessCommitted() {
820      if (!committed) {
821        try {
822          abort();
823        } catch (IOException ignored) {
824        }
825      }
826    }
827
828    private class FaultHidingOutputStream extends FilterOutputStream {
829      private FaultHidingOutputStream(OutputStream out) {
830        super(out);
831      }
832
833      @Override public void write(int oneByte) {
834        try {
835          out.write(oneByte);
836        } catch (IOException e) {
837          hasErrors = true;
838        }
839      }
840
841      @Override public void write(byte[] buffer, int offset, int length) {
842        try {
843          out.write(buffer, offset, length);
844        } catch (IOException e) {
845          hasErrors = true;
846        }
847      }
848
849      @Override public void close() {
850        try {
851          out.close();
852        } catch (IOException e) {
853          hasErrors = true;
854        }
855      }
856
857      @Override public void flush() {
858        try {
859          out.flush();
860        } catch (IOException e) {
861          hasErrors = true;
862        }
863      }
864    }
865  }
866
867  private final class Entry {
868    private final String key;
869
870    /** Lengths of this entry's files. */
871    private final long[] lengths;
872
873    /** True if this entry has ever been published. */
874    private boolean readable;
875
876    /** The ongoing edit or null if this entry is not being edited. */
877    private Editor currentEditor;
878
879    /** The sequence number of the most recently committed edit to this entry. */
880    private long sequenceNumber;
881
882    private Entry(String key) {
883      this.key = key;
884      this.lengths = new long[valueCount];
885    }
886
887    public String getLengths() throws IOException {
888      StringBuilder result = new StringBuilder();
889      for (long size : lengths) {
890        result.append(' ').append(size);
891      }
892      return result.toString();
893    }
894
895    /** Set lengths using decimal numbers like "10123". */
896    private void setLengths(String[] strings) throws IOException {
897      if (strings.length != valueCount) {
898        throw invalidLengths(strings);
899      }
900
901      try {
902        for (int i = 0; i < strings.length; i++) {
903          lengths[i] = Long.parseLong(strings[i]);
904        }
905      } catch (NumberFormatException e) {
906        throw invalidLengths(strings);
907      }
908    }
909
910    private IOException invalidLengths(String[] strings) throws IOException {
911      throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
912    }
913
914    public File getCleanFile(int i) {
915      return new File(directory, key + "." + i);
916    }
917
918    public File getDirtyFile(int i) {
919      return new File(directory, key + "." + i + ".tmp");
920    }
921  }
922}
923