• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.squareup.okhttp.internal;
18 
19 import java.io.Closeable;
20 import java.io.EOFException;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.FileNotFoundException;
24 import java.io.FileOutputStream;
25 import java.io.FilterOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.util.Iterator;
30 import java.util.LinkedHashMap;
31 import java.util.Map;
32 import java.util.concurrent.LinkedBlockingQueue;
33 import java.util.concurrent.ThreadPoolExecutor;
34 import java.util.concurrent.TimeUnit;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37 import okio.BufferedSink;
38 import okio.BufferedSource;
39 import okio.OkBuffer;
40 import 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  */
86 public 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 
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         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 
readJournal()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 
readJournalLine(String line)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    */
processJournal()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    */
rebuildJournal()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 
deleteIfExists(File file)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 
renameTo(File from, File to, boolean deleteDestination)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    */
get(String key)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    */
edit(String key)444   public Editor edit(String key) throws IOException {
445     return edit(key, ANY_SEQUENCE_NUMBER);
446   }
447 
edit(String key, long expectedSequenceNumber)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. */
getDirectory()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    */
getMaxSize()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    */
setMaxSize(long maxSize)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    */
size()499   public synchronized long size() {
500     return size;
501   }
502 
completeEdit(Editor editor, boolean success)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    */
journalRebuildRequired()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    */
remove(String key)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. */
isClosed()601   public boolean isClosed() {
602     return journalWriter == null;
603   }
604 
checkNotClosed()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. */
flush()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. */
close()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 
trimToSize()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    */
delete()647   public void delete() throws IOException {
648     close();
649     Util.deleteContents(directory);
650   }
651 
validateKey(String key)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 
inputStreamToString(InputStream in)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 
Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths)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      */
edit()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}. */
getInputStream(int index)688     public InputStream getInputStream(int index) {
689       return ins[index];
690     }
691 
692     /** Returns the string value for {@code index}. */
getString(int 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}. */
getLength(int index)698     public long getLength(int index) {
699       return lengths[index];
700     }
701 
close()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 
Editor(Entry entry)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      */
newInputStream(int index)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      */
getString(int index)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      */
newOutputStream(int index)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}. */
set(int index, String 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      */
commit()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      */
abort()815     public void abort() throws IOException {
816       completeEdit(this, false);
817     }
818 
abortUnlessCommitted()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 {
FaultHidingOutputStream(OutputStream out)829       private FaultHidingOutputStream(OutputStream out) {
830         super(out);
831       }
832 
write(int oneByte)833       @Override public void write(int oneByte) {
834         try {
835           out.write(oneByte);
836         } catch (IOException e) {
837           hasErrors = true;
838         }
839       }
840 
write(byte[] buffer, int offset, int length)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 
close()849       @Override public void close() {
850         try {
851           out.close();
852         } catch (IOException e) {
853           hasErrors = true;
854         }
855       }
856 
flush()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 
Entry(String key)882     private Entry(String key) {
883       this.key = key;
884       this.lengths = new long[valueCount];
885     }
886 
getLengths()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". */
setLengths(String[] strings)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 
invalidLengths(String[] strings)910     private IOException invalidLengths(String[] strings) throws IOException {
911       throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
912     }
913 
getCleanFile(int i)914     public File getCleanFile(int i) {
915       return new File(directory, key + "." + i);
916     }
917 
getDirtyFile(int i)918     public File getDirtyFile(int i) {
919       return new File(directory, key + "." + i + ".tmp");
920     }
921   }
922 }
923