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 com.squareup.okhttp.internal.io.FileSystem;
20import java.io.Closeable;
21import java.io.EOFException;
22import java.io.File;
23import java.io.FileNotFoundException;
24import java.io.IOException;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.Iterator;
28import java.util.LinkedHashMap;
29import java.util.NoSuchElementException;
30import java.util.concurrent.Executor;
31import java.util.concurrent.LinkedBlockingQueue;
32import java.util.concurrent.ThreadPoolExecutor;
33import java.util.concurrent.TimeUnit;
34import java.util.regex.Matcher;
35import java.util.regex.Pattern;
36import okio.Buffer;
37import okio.BufferedSink;
38import okio.BufferedSource;
39import okio.Okio;
40import okio.Sink;
41import okio.Source;
42import okio.Timeout;
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,120}");
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 FileSystem fileSystem;
142  private final File directory;
143  private final File journalFile;
144  private final File journalFileTmp;
145  private final File journalFileBackup;
146  private final int appVersion;
147  private long maxSize;
148  private final int valueCount;
149  private long size = 0;
150  private BufferedSink journalWriter;
151  private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
152  private int redundantOpCount;
153  private boolean hasJournalErrors;
154
155  // Must be read and written when synchronized on 'this'.
156  private boolean initialized;
157  private boolean closed;
158
159  /**
160   * To differentiate between old and current snapshots, each entry is given
161   * a sequence number each time an edit is committed. A snapshot is stale if
162   * its sequence number is not equal to its entry's sequence number.
163   */
164  private long nextSequenceNumber = 0;
165
166  /** Used to run 'cleanupRunnable' for journal rebuilds. */
167  private final Executor executor;
168  private final Runnable cleanupRunnable = new Runnable() {
169    public void run() {
170      synchronized (DiskLruCache.this) {
171        if (!initialized | closed) {
172          return; // Nothing to do
173        }
174        try {
175          trimToSize();
176          if (journalRebuildRequired()) {
177            rebuildJournal();
178            redundantOpCount = 0;
179          }
180        } catch (IOException e) {
181          throw new RuntimeException(e);
182        }
183      }
184    }
185  };
186
187  DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize,
188      Executor executor) {
189    this.fileSystem = fileSystem;
190    this.directory = directory;
191    this.appVersion = appVersion;
192    this.journalFile = new File(directory, JOURNAL_FILE);
193    this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
194    this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
195    this.valueCount = valueCount;
196    this.maxSize = maxSize;
197    this.executor = executor;
198  }
199
200  public synchronized void initialize() throws IOException {
201    assert Thread.holdsLock(this);
202
203    if (initialized) {
204      return; // Already initialized.
205    }
206
207    // If a bkp file exists, use it instead.
208    if (fileSystem.exists(journalFileBackup)) {
209      // If journal file also exists just delete backup file.
210      if (fileSystem.exists(journalFile)) {
211        fileSystem.delete(journalFileBackup);
212      } else {
213        fileSystem.rename(journalFileBackup, journalFile);
214      }
215    }
216
217    // Prefer to pick up where we left off.
218    if (fileSystem.exists(journalFile)) {
219      try {
220        readJournal();
221        processJournal();
222        initialized = true;
223        return;
224      } catch (IOException journalIsCorrupt) {
225        Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
226            + journalIsCorrupt.getMessage() + ", removing");
227        delete();
228        closed = false;
229      }
230    }
231
232    rebuildJournal();
233
234    initialized = true;
235  }
236
237  /**
238   * Create a cache which will reside in {@code directory}. This cache is lazily initialized on
239   * first access and will be created if it does not exist.
240   *
241   * @param directory a writable directory
242   * @param valueCount the number of values per cache entry. Must be positive.
243   * @param maxSize the maximum number of bytes this cache should use to store
244   */
245  public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,
246      int valueCount, long maxSize) {
247    if (maxSize <= 0) {
248      throw new IllegalArgumentException("maxSize <= 0");
249    }
250    if (valueCount <= 0) {
251      throw new IllegalArgumentException("valueCount <= 0");
252    }
253
254    // Use a single background thread to evict entries.
255    Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
256        new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
257
258    return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
259  }
260
261  private void readJournal() throws IOException {
262    BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
263    try {
264      String magic = source.readUtf8LineStrict();
265      String version = source.readUtf8LineStrict();
266      String appVersionString = source.readUtf8LineStrict();
267      String valueCountString = source.readUtf8LineStrict();
268      String blank = source.readUtf8LineStrict();
269      if (!MAGIC.equals(magic)
270          || !VERSION_1.equals(version)
271          || !Integer.toString(appVersion).equals(appVersionString)
272          || !Integer.toString(valueCount).equals(valueCountString)
273          || !"".equals(blank)) {
274        throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
275            + valueCountString + ", " + blank + "]");
276      }
277
278      int lineCount = 0;
279      while (true) {
280        try {
281          readJournalLine(source.readUtf8LineStrict());
282          lineCount++;
283        } catch (EOFException endOfJournal) {
284          break;
285        }
286      }
287      redundantOpCount = lineCount - lruEntries.size();
288
289      // If we ended on a truncated line, rebuild the journal before appending to it.
290      if (!source.exhausted()) {
291        rebuildJournal();
292      } else {
293        journalWriter = newJournalWriter();
294      }
295    } finally {
296      Util.closeQuietly(source);
297    }
298  }
299
300  private BufferedSink newJournalWriter() throws FileNotFoundException {
301    Sink fileSink = fileSystem.appendingSink(journalFile);
302    Sink faultHidingSink = new FaultHidingSink(fileSink) {
303      @Override protected void onException(IOException e) {
304        assert (Thread.holdsLock(DiskLruCache.this));
305        hasJournalErrors = true;
306      }
307    };
308    return Okio.buffer(faultHidingSink);
309  }
310
311  private void readJournalLine(String line) throws IOException {
312    int firstSpace = line.indexOf(' ');
313    if (firstSpace == -1) {
314      throw new IOException("unexpected journal line: " + line);
315    }
316
317    int keyBegin = firstSpace + 1;
318    int secondSpace = line.indexOf(' ', keyBegin);
319    final String key;
320    if (secondSpace == -1) {
321      key = line.substring(keyBegin);
322      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
323        lruEntries.remove(key);
324        return;
325      }
326    } else {
327      key = line.substring(keyBegin, secondSpace);
328    }
329
330    Entry entry = lruEntries.get(key);
331    if (entry == null) {
332      entry = new Entry(key);
333      lruEntries.put(key, entry);
334    }
335
336    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
337      String[] parts = line.substring(secondSpace + 1).split(" ");
338      entry.readable = true;
339      entry.currentEditor = null;
340      entry.setLengths(parts);
341    } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
342      entry.currentEditor = new Editor(entry);
343    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
344      // This work was already done by calling lruEntries.get().
345    } else {
346      throw new IOException("unexpected journal line: " + line);
347    }
348  }
349
350  /**
351   * Computes the initial size and collects garbage as a part of opening the
352   * cache. Dirty entries are assumed to be inconsistent and will be deleted.
353   */
354  private void processJournal() throws IOException {
355    fileSystem.delete(journalFileTmp);
356    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
357      Entry entry = i.next();
358      if (entry.currentEditor == null) {
359        for (int t = 0; t < valueCount; t++) {
360          size += entry.lengths[t];
361        }
362      } else {
363        entry.currentEditor = null;
364        for (int t = 0; t < valueCount; t++) {
365          fileSystem.delete(entry.cleanFiles[t]);
366          fileSystem.delete(entry.dirtyFiles[t]);
367        }
368        i.remove();
369      }
370    }
371  }
372
373  /**
374   * Creates a new journal that omits redundant information. This replaces the
375   * current journal if it exists.
376   */
377  private synchronized void rebuildJournal() throws IOException {
378    if (journalWriter != null) {
379      journalWriter.close();
380    }
381
382    BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
383    try {
384      writer.writeUtf8(MAGIC).writeByte('\n');
385      writer.writeUtf8(VERSION_1).writeByte('\n');
386      writer.writeDecimalLong(appVersion).writeByte('\n');
387      writer.writeDecimalLong(valueCount).writeByte('\n');
388      writer.writeByte('\n');
389
390      for (Entry entry : lruEntries.values()) {
391        if (entry.currentEditor != null) {
392          writer.writeUtf8(DIRTY).writeByte(' ');
393          writer.writeUtf8(entry.key);
394          writer.writeByte('\n');
395        } else {
396          writer.writeUtf8(CLEAN).writeByte(' ');
397          writer.writeUtf8(entry.key);
398          entry.writeLengths(writer);
399          writer.writeByte('\n');
400        }
401      }
402    } finally {
403      writer.close();
404    }
405
406    if (fileSystem.exists(journalFile)) {
407      fileSystem.rename(journalFile, journalFileBackup);
408    }
409    fileSystem.rename(journalFileTmp, journalFile);
410    fileSystem.delete(journalFileBackup);
411
412    journalWriter = newJournalWriter();
413    hasJournalErrors = false;
414  }
415
416  /**
417   * Returns a snapshot of the entry named {@code key}, or null if it doesn't
418   * exist is not currently readable. If a value is returned, it is moved to
419   * the head of the LRU queue.
420   */
421  public synchronized Snapshot get(String key) throws IOException {
422    initialize();
423
424    checkNotClosed();
425    validateKey(key);
426    Entry entry = lruEntries.get(key);
427    if (entry == null || !entry.readable) return null;
428
429    Snapshot snapshot = entry.snapshot();
430    if (snapshot == null) return null;
431
432    redundantOpCount++;
433    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
434    if (journalRebuildRequired()) {
435      executor.execute(cleanupRunnable);
436    }
437
438    return snapshot;
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    initialize();
451
452    checkNotClosed();
453    validateKey(key);
454    Entry entry = lruEntries.get(key);
455    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
456        || entry.sequenceNumber != expectedSequenceNumber)) {
457      return null; // Snapshot is stale.
458    }
459    if (entry != null && entry.currentEditor != null) {
460      return null; // Another edit is in progress.
461    }
462
463    // Flush the journal before creating files to prevent file leaks.
464    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
465    journalWriter.flush();
466
467    if (hasJournalErrors) {
468      return null; // Don't edit; the journal can't be written.
469    }
470
471    if (entry == null) {
472      entry = new Entry(key);
473      lruEntries.put(key, entry);
474    }
475    Editor editor = new Editor(entry);
476    entry.currentEditor = editor;
477    return editor;
478  }
479
480  /** Returns the directory where this cache stores its data. */
481  public File getDirectory() {
482    return directory;
483  }
484
485  /**
486   * Returns the maximum number of bytes that this cache should use to store
487   * its data.
488   */
489  public synchronized long getMaxSize() {
490    return maxSize;
491  }
492
493  /**
494   * Changes the maximum number of bytes the cache can store and queues a job
495   * to trim the existing store, if necessary.
496   */
497  public synchronized void setMaxSize(long maxSize) {
498    this.maxSize = maxSize;
499    if (initialized) {
500      executor.execute(cleanupRunnable);
501    }
502  }
503
504  /**
505   * Returns the number of bytes currently being used to store the values in
506   * this cache. This may be greater than the max size if a background
507   * deletion is pending.
508   */
509  public synchronized long size() throws IOException {
510    initialize();
511    return size;
512  }
513
514  private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
515    Entry entry = editor.entry;
516    if (entry.currentEditor != editor) {
517      throw new IllegalStateException();
518    }
519
520    // If this edit is creating the entry for the first time, every index must have a value.
521    if (success && !entry.readable) {
522      for (int i = 0; i < valueCount; i++) {
523        if (!editor.written[i]) {
524          editor.abort();
525          throw new IllegalStateException("Newly created entry didn't create value for index " + i);
526        }
527        if (!fileSystem.exists(entry.dirtyFiles[i])) {
528          editor.abort();
529          return;
530        }
531      }
532    }
533
534    for (int i = 0; i < valueCount; i++) {
535      File dirty = entry.dirtyFiles[i];
536      if (success) {
537        if (fileSystem.exists(dirty)) {
538          File clean = entry.cleanFiles[i];
539          fileSystem.rename(dirty, clean);
540          long oldLength = entry.lengths[i];
541          long newLength = fileSystem.size(clean);
542          entry.lengths[i] = newLength;
543          size = size - oldLength + newLength;
544        }
545      } else {
546        fileSystem.delete(dirty);
547      }
548    }
549
550    redundantOpCount++;
551    entry.currentEditor = null;
552    if (entry.readable | success) {
553      entry.readable = true;
554      journalWriter.writeUtf8(CLEAN).writeByte(' ');
555      journalWriter.writeUtf8(entry.key);
556      entry.writeLengths(journalWriter);
557      journalWriter.writeByte('\n');
558      if (success) {
559        entry.sequenceNumber = nextSequenceNumber++;
560      }
561    } else {
562      lruEntries.remove(entry.key);
563      journalWriter.writeUtf8(REMOVE).writeByte(' ');
564      journalWriter.writeUtf8(entry.key);
565      journalWriter.writeByte('\n');
566    }
567    journalWriter.flush();
568
569    if (size > maxSize || journalRebuildRequired()) {
570      executor.execute(cleanupRunnable);
571    }
572  }
573
574  /**
575   * We only rebuild the journal when it will halve the size of the journal
576   * and eliminate at least 2000 ops.
577   */
578  private boolean journalRebuildRequired() {
579    final int redundantOpCompactThreshold = 2000;
580    return redundantOpCount >= redundantOpCompactThreshold
581        && redundantOpCount >= lruEntries.size();
582  }
583
584  /**
585   * Drops the entry for {@code key} if it exists and can be removed. If the
586   * entry for {@code key} is currently being edited, that edit will complete
587   * normally but its value will not be stored.
588   *
589   * @return true if an entry was removed.
590   */
591  public synchronized boolean remove(String key) throws IOException {
592    initialize();
593
594    checkNotClosed();
595    validateKey(key);
596    Entry entry = lruEntries.get(key);
597    if (entry == null) return false;
598    return removeEntry(entry);
599  }
600
601  private boolean removeEntry(Entry entry) throws IOException {
602    if (entry.currentEditor != null) {
603      entry.currentEditor.detach(); // Prevent the edit from completing normally.
604    }
605
606    for (int i = 0; i < valueCount; i++) {
607      fileSystem.delete(entry.cleanFiles[i]);
608      size -= entry.lengths[i];
609      entry.lengths[i] = 0;
610    }
611
612    redundantOpCount++;
613    journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
614    lruEntries.remove(entry.key);
615
616    if (journalRebuildRequired()) {
617      executor.execute(cleanupRunnable);
618    }
619
620    return true;
621  }
622
623  /** Returns true if this cache has been closed. */
624  public synchronized boolean isClosed() {
625    return closed;
626  }
627
628  private synchronized void checkNotClosed() {
629    if (isClosed()) {
630      throw new IllegalStateException("cache is closed");
631    }
632  }
633
634  /** Force buffered operations to the filesystem. */
635  public synchronized void flush() throws IOException {
636    if (!initialized) return;
637
638    checkNotClosed();
639    trimToSize();
640    journalWriter.flush();
641  }
642
643  /** Closes this cache. Stored values will remain on the filesystem. */
644  public synchronized void close() throws IOException {
645    if (!initialized || closed) {
646      closed = true;
647      return;
648    }
649    // Copying for safe iteration.
650    for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) {
651      if (entry.currentEditor != null) {
652        entry.currentEditor.abort();
653      }
654    }
655    trimToSize();
656    journalWriter.close();
657    journalWriter = null;
658    closed = true;
659  }
660
661  private void trimToSize() throws IOException {
662    while (size > maxSize) {
663      Entry toEvict = lruEntries.values().iterator().next();
664      removeEntry(toEvict);
665    }
666  }
667
668  /**
669   * Closes the cache and deletes all of its stored values. This will delete
670   * all files in the cache directory including files that weren't created by
671   * the cache.
672   */
673  public void delete() throws IOException {
674    close();
675    fileSystem.deleteContents(directory);
676  }
677
678  /**
679   * Deletes all stored values from the cache. In-flight edits will complete
680   * normally but their values will not be stored.
681   */
682  public synchronized void evictAll() throws IOException {
683    initialize();
684    // Copying for safe iteration.
685    for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) {
686      removeEntry(entry);
687    }
688  }
689
690  private void validateKey(String key) {
691    Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
692    if (!matcher.matches()) {
693      throw new IllegalArgumentException(
694          "keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"");
695    }
696  }
697
698  /**
699   * Returns an iterator over the cache's current entries. This iterator doesn't throw {@code
700   * ConcurrentModificationException}, but if new entries are added while iterating, those new
701   * entries will not be returned by the iterator. If existing entries are removed during iteration,
702   * they will be absent (unless they were already returned).
703   *
704   * <p>If there are I/O problems during iteration, this iterator fails silently. For example, if
705   * the hosting filesystem becomes unreachable, the iterator will omit elements rather than
706   * throwing exceptions.
707   *
708   * <p><strong>The caller must {@link Snapshot#close close}</strong> each snapshot returned by
709   * {@link Iterator#next}. Failing to do so leaks open files!
710   *
711   * <p>The returned iterator supports {@link Iterator#remove}.
712   */
713  public synchronized Iterator<Snapshot> snapshots() throws IOException {
714    initialize();
715    return new Iterator<Snapshot>() {
716      /** Iterate a copy of the entries to defend against concurrent modification errors. */
717      final Iterator<Entry> delegate = new ArrayList<>(lruEntries.values()).iterator();
718
719      /** The snapshot to return from {@link #next}. Null if we haven't computed that yet. */
720      Snapshot nextSnapshot;
721
722      /** The snapshot to remove with {@link #remove}. Null if removal is illegal. */
723      Snapshot removeSnapshot;
724
725      @Override public boolean hasNext() {
726        if (nextSnapshot != null) return true;
727
728        synchronized (DiskLruCache.this) {
729          // If the cache is closed, truncate the iterator.
730          if (closed) return false;
731
732          while (delegate.hasNext()) {
733            Entry entry = delegate.next();
734            Snapshot snapshot = entry.snapshot();
735            if (snapshot == null) continue; // Evicted since we copied the entries.
736            nextSnapshot = snapshot;
737            return true;
738          }
739        }
740
741        return false;
742      }
743
744      @Override public Snapshot next() {
745        if (!hasNext()) throw new NoSuchElementException();
746        removeSnapshot = nextSnapshot;
747        nextSnapshot = null;
748        return removeSnapshot;
749      }
750
751      @Override public void remove() {
752        if (removeSnapshot == null) throw new IllegalStateException("remove() before next()");
753        try {
754          DiskLruCache.this.remove(removeSnapshot.key);
755        } catch (IOException ignored) {
756          // Nothing useful to do here. We failed to remove from the cache. Most likely that's
757          // because we couldn't update the journal, but the cached entry will still be gone.
758        } finally {
759          removeSnapshot = null;
760        }
761      }
762    };
763  }
764
765  /** A snapshot of the values for an entry. */
766  public final class Snapshot implements Closeable {
767    private final String key;
768    private final long sequenceNumber;
769    private final Source[] sources;
770    private final long[] lengths;
771
772    private Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
773      this.key = key;
774      this.sequenceNumber = sequenceNumber;
775      this.sources = sources;
776      this.lengths = lengths;
777    }
778
779    public String key() {
780      return key;
781    }
782
783    /**
784     * Returns an editor for this snapshot's entry, or null if either the
785     * entry has changed since this snapshot was created or if another edit
786     * is in progress.
787     */
788    public Editor edit() throws IOException {
789      return DiskLruCache.this.edit(key, sequenceNumber);
790    }
791
792    /** Returns the unbuffered stream with the value for {@code index}. */
793    public Source getSource(int index) {
794      return sources[index];
795    }
796
797    /** Returns the byte length of the value for {@code index}. */
798    public long getLength(int index) {
799      return lengths[index];
800    }
801
802    public void close() {
803      for (Source in : sources) {
804        Util.closeQuietly(in);
805      }
806    }
807  }
808
809  private static final Sink NULL_SINK = new Sink() {
810    @Override public void write(Buffer source, long byteCount) throws IOException {
811      source.skip(byteCount);
812    }
813
814    @Override public void flush() throws IOException {
815    }
816
817    @Override public Timeout timeout() {
818      return Timeout.NONE;
819    }
820
821    @Override public void close() throws IOException {
822    }
823  };
824
825  /** Edits the values for an entry. */
826  public final class Editor {
827    private final Entry entry;
828    private final boolean[] written;
829    private boolean done;
830
831    private Editor(Entry entry) {
832      this.entry = entry;
833      this.written = (entry.readable) ? null : new boolean[valueCount];
834    }
835
836    /**
837     * Prevents this editor from completing normally. This is necessary either when the edit causes
838     * an I/O error, or if the target entry is evicted while this editor is active. In either case
839     * we delete the editor's created files and prevent new files from being created. Note that once
840     * an editor has been detached it is possible for another editor to edit the entry.
841     */
842    void detach() {
843      if (entry.currentEditor == this) {
844        for (int i = 0; i < valueCount; i++) {
845          try {
846            fileSystem.delete(entry.dirtyFiles[i]);
847          } catch (IOException e) {
848            // This file is potentially leaked. Not much we can do about that.
849          }
850        }
851        entry.currentEditor = null;
852      }
853    }
854
855    /**
856     * Returns an unbuffered input stream to read the last committed value,
857     * or null if no value has been committed.
858     */
859    public Source newSource(int index) throws IOException {
860      synchronized (DiskLruCache.this) {
861        if (done) {
862          throw new IllegalStateException();
863        }
864        if (!entry.readable || entry.currentEditor != this) {
865          return null;
866        }
867        try {
868          return fileSystem.source(entry.cleanFiles[index]);
869        } catch (FileNotFoundException e) {
870          return null;
871        }
872      }
873    }
874
875    /**
876     * Returns a new unbuffered output stream to write the value at
877     * {@code index}. If the underlying output stream encounters errors
878     * when writing to the filesystem, this edit will be aborted when
879     * {@link #commit} is called. The returned output stream does not throw
880     * IOExceptions.
881     */
882    public Sink newSink(int index) throws IOException {
883      synchronized (DiskLruCache.this) {
884        if (done) {
885          throw new IllegalStateException();
886        }
887        if (entry.currentEditor != this) {
888          return NULL_SINK;
889        }
890        if (!entry.readable) {
891          written[index] = true;
892        }
893        File dirtyFile = entry.dirtyFiles[index];
894        Sink sink;
895        try {
896          sink = fileSystem.sink(dirtyFile);
897        } catch (FileNotFoundException e) {
898          return NULL_SINK;
899        }
900        return new FaultHidingSink(sink) {
901          @Override protected void onException(IOException e) {
902            synchronized (DiskLruCache.this) {
903              detach();
904            }
905          }
906        };
907      }
908    }
909
910    /**
911     * Commits this edit so it is visible to readers.  This releases the
912     * edit lock so another edit may be started on the same key.
913     */
914    public void commit() throws IOException {
915      synchronized (DiskLruCache.this) {
916        if (done) {
917          throw new IllegalStateException();
918        }
919        if (entry.currentEditor == this) {
920          completeEdit(this, true);
921        }
922        done = true;
923      }
924    }
925
926    /**
927     * Aborts this edit. This releases the edit lock so another edit may be
928     * started on the same key.
929     */
930    public void abort() throws IOException {
931      synchronized (DiskLruCache.this) {
932        if (done) {
933          throw new IllegalStateException();
934        }
935        if (entry.currentEditor == this) {
936          completeEdit(this, false);
937        }
938        done = true;
939      }
940    }
941
942    public void abortUnlessCommitted() {
943      synchronized (DiskLruCache.this) {
944        if (!done && entry.currentEditor == this) {
945          try {
946            completeEdit(this, false);
947          } catch (IOException ignored) {
948          }
949        }
950      }
951    }
952  }
953
954  private final class Entry {
955    private final String key;
956
957    /** Lengths of this entry's files. */
958    private final long[] lengths;
959    private final File[] cleanFiles;
960    private final File[] dirtyFiles;
961
962    /** True if this entry has ever been published. */
963    private boolean readable;
964
965    /** The ongoing edit or null if this entry is not being edited. */
966    private Editor currentEditor;
967
968    /** The sequence number of the most recently committed edit to this entry. */
969    private long sequenceNumber;
970
971    private Entry(String key) {
972      this.key = key;
973
974      lengths = new long[valueCount];
975      cleanFiles = new File[valueCount];
976      dirtyFiles = new File[valueCount];
977
978      // The names are repetitive so re-use the same builder to avoid allocations.
979      StringBuilder fileBuilder = new StringBuilder(key).append('.');
980      int truncateTo = fileBuilder.length();
981      for (int i = 0; i < valueCount; i++) {
982        fileBuilder.append(i);
983        cleanFiles[i] = new File(directory, fileBuilder.toString());
984        fileBuilder.append(".tmp");
985        dirtyFiles[i] = new File(directory, fileBuilder.toString());
986        fileBuilder.setLength(truncateTo);
987      }
988    }
989
990    /** Set lengths using decimal numbers like "10123". */
991    private void setLengths(String[] strings) throws IOException {
992      if (strings.length != valueCount) {
993        throw invalidLengths(strings);
994      }
995
996      try {
997        for (int i = 0; i < strings.length; i++) {
998          lengths[i] = Long.parseLong(strings[i]);
999        }
1000      } catch (NumberFormatException e) {
1001        throw invalidLengths(strings);
1002      }
1003    }
1004
1005    /** Append space-prefixed lengths to {@code writer}. */
1006    void writeLengths(BufferedSink writer) throws IOException {
1007      for (long length : lengths) {
1008        writer.writeByte(' ').writeDecimalLong(length);
1009      }
1010    }
1011
1012    private IOException invalidLengths(String[] strings) throws IOException {
1013      throw new IOException("unexpected journal line: " + Arrays.toString(strings));
1014    }
1015
1016    /**
1017     * Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
1018     * single published snapshot. If we opened streams lazily then the streams could come from
1019     * different edits.
1020     */
1021    Snapshot snapshot() {
1022      if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
1023
1024      Source[] sources = new Source[valueCount];
1025      long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
1026      try {
1027        for (int i = 0; i < valueCount; i++) {
1028          sources[i] = fileSystem.source(cleanFiles[i]);
1029        }
1030        return new Snapshot(key, sequenceNumber, sources, lengths);
1031      } catch (FileNotFoundException e) {
1032        // A file must have been deleted manually!
1033        for (int i = 0; i < valueCount; i++) {
1034          if (sources[i] != null) {
1035            Util.closeQuietly(sources[i]);
1036          } else {
1037            break;
1038          }
1039        }
1040        return null;
1041      }
1042    }
1043  }
1044}
1045