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