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