OSDN Git Service

eaf47b1b418c659dd749da318bc14f96a287e352
[android-x86/packages-apps-Eleven.git] / src / org / lineageos / eleven / cache / DiskLruCache.java
1 /*
2  * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
3  * License, Version 2.0 (the "License"); you may not use this file except in
4  * compliance with the License. You may obtain a copy of the License at
5  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6  * or agreed to in writing, software distributed under the License is
7  * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8  * KIND, either express or implied. See the License for the specific language
9  * governing permissions and limitations under the License.
10  */
11
12 package org.lineageos.eleven.cache;
13
14 import java.io.BufferedInputStream;
15 import java.io.BufferedWriter;
16 import java.io.Closeable;
17 import java.io.EOFException;
18 import java.io.File;
19 import java.io.FileInputStream;
20 import java.io.FileNotFoundException;
21 import java.io.FileOutputStream;
22 import java.io.FileWriter;
23 import java.io.FilterOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.InputStreamReader;
27 import java.io.OutputStream;
28 import java.io.OutputStreamWriter;
29 import java.io.Reader;
30 import java.io.StringWriter;
31 import java.io.Writer;
32 import java.lang.reflect.Array;
33 import java.nio.charset.Charset;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Iterator;
37 import java.util.LinkedHashMap;
38 import java.util.Map;
39 import java.util.concurrent.Callable;
40 import java.util.concurrent.ExecutorService;
41 import java.util.concurrent.LinkedBlockingQueue;
42 import java.util.concurrent.ThreadPoolExecutor;
43 import java.util.concurrent.TimeUnit;
44
45 /**
46  ****************************************************************************** Taken from the JB source code, can be found in:
47  * libcore/luni/src/main/java/libcore/io/DiskLruCache.java or direct link:
48  * https:
49  * //android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/
50  * main/java/libcore/io/DiskLruCache.java A cache that uses a bounded amount of
51  * space on a filesystem. Each cache entry has a string key and a fixed number
52  * of values. Values are byte sequences, accessible as streams or files. Each
53  * value must be between {@code 0} and {@code Integer.MAX_VALUE} bytes in
54  * length.
55  * <p>
56  * The cache stores its data in a directory on the filesystem. This directory
57  * must be exclusive to the cache; the cache may delete or overwrite files from
58  * its directory. It is an error for multiple processes to use the same cache
59  * directory at the same time.
60  * <p>
61  * This cache limits the number of bytes that it will store on the filesystem.
62  * When the number of stored bytes exceeds the limit, the cache will remove
63  * entries in the background until the limit is satisfied. The limit is not
64  * strict: the cache may temporarily exceed it while waiting for files to be
65  * deleted. The limit does not include filesystem overhead or the cache journal
66  * so space-sensitive applications should set a conservative limit.
67  * <p>
68  * Clients call {@link #edit} to create or update the values of an entry. An
69  * entry may have only one editor at one time; if a value is not available to be
70  * edited then {@link #edit} will return null.
71  * <ul>
72  * <li>When an entry is being <strong>created</strong> it is necessary to supply
73  * a full set of values; the empty value should be used as a placeholder if
74  * necessary.
75  * <li>When an entry is being <strong>edited</strong>, it is not necessary to
76  * supply data for every value; values default to their previous value.
77  * </ul>
78  * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
79  * or {@link Editor#abort}. Committing is atomic: a read observes the full set
80  * of values as they were before or after the commit, but never a mix of values.
81  * <p>
82  * Clients call {@link #get} to read a snapshot of an entry. The read will
83  * observe the value at the time that {@link #get} was called. Updates and
84  * removals after the call do not impact ongoing reads.
85  * <p>
86  * This class is tolerant of some I/O errors. If files are missing from the
87  * filesystem, the corresponding entries will be dropped from the cache. If an
88  * error occurs while writing a cache value, the edit will fail silently.
89  * Callers should handle other problems by catching {@code IOException} and
90  * responding appropriately.
91  */
92 public final class DiskLruCache implements Closeable {
93     static final String JOURNAL_FILE = "journal";
94
95     static final String JOURNAL_FILE_TMP = "journal.tmp";
96
97     static final String MAGIC = "libcore.io.DiskLruCache";
98
99     static final String VERSION_1 = "1";
100
101     static final long ANY_SEQUENCE_NUMBER = -1;
102
103     private static final String CLEAN = "CLEAN";
104
105     private static final String DIRTY = "DIRTY";
106
107     private static final String REMOVE = "REMOVE";
108
109     private static final String READ = "READ";
110
111     private static final Charset UTF_8 = Charset.forName("UTF-8");
112
113     private static final int IO_BUFFER_SIZE = 8 * 1024;
114
115     /*
116      * This cache uses a journal file named "journal". A typical journal file
117      * looks like this: libcore.io.DiskLruCache 1 100 2 CLEAN
118      * 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 DIRTY
119      * 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52
120      * 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY
121      * 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a
122      * 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ
123      * 3400330d1dfc7f3f7f4b8d4d803dfcf6 The first five lines of the journal form
124      * its header. They are the constant string "libcore.io.DiskLruCache", the
125      * disk cache's version, the application's version, the value count, and a
126      * blank line. Each of the subsequent lines in the file is a record of the
127      * state of a cache entry. Each line contains space-separated values: a
128      * state, a key, and optional state-specific values. o DIRTY lines track
129      * that an entry is actively being created or updated. Every successful
130      * DIRTY action should be followed by a CLEAN or REMOVE action. DIRTY lines
131      * without a matching CLEAN or REMOVE indicate that temporary files may need
132      * to be deleted. o CLEAN lines track a cache entry that has been
133      * successfully published and may be read. A publish line is followed by the
134      * lengths of each of its values. o READ lines track accesses for LRU. o
135      * REMOVE lines track entries that have been deleted. The journal file is
136      * appended to as cache operations occur. The journal may occasionally be
137      * compacted by dropping redundant lines. A temporary file named
138      * "journal.tmp" will be used during compaction; that file should be deleted
139      * if it exists when the cache is opened.
140      */
141
142     private final File directory;
143
144     private final File journalFile;
145
146     private final File journalFileTmp;
147
148     private final int appVersion;
149
150     private final long maxSize;
151
152     private final int valueCount;
153
154     private long size = 0;
155
156     private Writer journalWriter;
157
158     private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0,
159             0.75f, true);
160
161     private int redundantOpCount;
162
163     /**
164      * To differentiate between old and current snapshots, each entry is given a
165      * sequence number each time an edit is committed. A snapshot is stale if
166      * its sequence number is not equal to its entry's sequence number.
167      */
168     private long nextSequenceNumber = 0;
169
170     /* From java.util.Arrays */
171     @SuppressWarnings("unchecked")
172     private static <T> T[] copyOfRange(final T[] original, final int start, final int end) {
173         final int originalLength = original.length; // For exception priority
174                                                     // compatibility.
175         if (start > end) {
176             throw new IllegalArgumentException();
177         }
178         if (start < 0 || start > originalLength) {
179             throw new ArrayIndexOutOfBoundsException();
180         }
181         final int resultLength = end - start;
182         final int copyLength = Math.min(resultLength, originalLength - start);
183         final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(),
184                 resultLength);
185         System.arraycopy(original, start, result, 0, copyLength);
186         return result;
187     }
188
189     /**
190      * Returns the remainder of 'reader' as a string, closing it when done.
191      */
192     public static String readFully(final Reader reader) throws IOException {
193         try {
194             final StringWriter writer = new StringWriter();
195             final char[] buffer = new char[1024];
196             int count;
197             while ((count = reader.read(buffer)) != -1) {
198                 writer.write(buffer, 0, count);
199             }
200             return writer.toString();
201         } finally {
202             reader.close();
203         }
204     }
205
206     /**
207      * Returns the ASCII characters up to but not including the next "\r\n", or
208      * "\n".
209      *
210      * @throws java.io.EOFException if the stream is exhausted before the next
211      *             newline character.
212      */
213     public static String readAsciiLine(final InputStream in) throws IOException {
214         // TODO: support UTF-8 here instead
215
216         final StringBuilder result = new StringBuilder(80);
217         while (true) {
218             final int c = in.read();
219             if (c == -1) {
220                 throw new EOFException();
221             } else if (c == '\n') {
222                 break;
223             }
224
225             result.append((char)c);
226         }
227         final int length = result.length();
228         if (length > 0 && result.charAt(length - 1) == '\r') {
229             result.setLength(length - 1);
230         }
231         return result.toString();
232     }
233
234     /**
235      * Closes 'closeable', ignoring any checked exceptions. Does nothing if
236      * 'closeable' is null.
237      */
238     public static void closeQuietly(final Closeable closeable) {
239         if (closeable != null) {
240             try {
241                 closeable.close();
242             } catch (final RuntimeException rethrown) {
243                 throw rethrown;
244             } catch (final Exception ignored) {
245             }
246         }
247     }
248
249     /**
250      * Recursively delete everything in {@code dir}.
251      */
252     // TODO: this should specify paths as Strings rather than as Files
253     public static void deleteContents(final File dir) throws IOException {
254         final File[] files = dir.listFiles();
255         if (files == null) {
256             throw new IllegalArgumentException("not a directory: " + dir);
257         }
258         for (final File file : files) {
259             if (file.isDirectory()) {
260                 deleteContents(file);
261             }
262             if (!file.delete()) {
263                 throw new IOException("failed to delete file: " + file);
264             }
265         }
266     }
267
268     /** This cache uses a single background thread to evict entries. */
269     private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L,
270             TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
271
272     private final Callable<Void> cleanupCallable = new Callable<Void>() {
273         @Override
274         public Void call() throws Exception {
275             synchronized (DiskLruCache.this) {
276                 if (journalWriter == null) {
277                     return null; // closed
278                 }
279                 trimToSize();
280                 if (journalRebuildRequired()) {
281                     rebuildJournal();
282                     redundantOpCount = 0;
283                 }
284             }
285             return null;
286         }
287     };
288
289     private DiskLruCache(final File directory, final int appVersion, final int valueCount,
290             final long maxSize) {
291         this.directory = directory;
292         this.appVersion = appVersion;
293         journalFile = new File(directory, JOURNAL_FILE);
294         journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
295         this.valueCount = valueCount;
296         this.maxSize = maxSize;
297     }
298
299     /**
300      * Opens the cache in {@code directory}, creating a cache if none exists
301      * there.
302      *
303      * @param directory a writable directory
304      * @param appVersion
305      * @param valueCount the number of values per cache entry. Must be positive.
306      * @param maxSize the maximum number of bytes this cache should use to store
307      * @throws IOException if reading or writing the cache directory fails
308      */
309     public static DiskLruCache open(final File directory, final int appVersion,
310             final int valueCount, final long maxSize) throws IOException {
311         if (maxSize <= 0) {
312             throw new IllegalArgumentException("maxSize <= 0");
313         }
314         if (valueCount <= 0) {
315             throw new IllegalArgumentException("valueCount <= 0");
316         }
317
318         // prefer to pick up where we left off
319         DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
320         if (cache.journalFile.exists()) {
321             try {
322                 cache.readJournal();
323                 cache.processJournal();
324                 cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
325                         IO_BUFFER_SIZE);
326                 return cache;
327             } catch (final IOException journalIsCorrupt) {
328                 // System.logW("DiskLruCache " + directory + " is corrupt: "
329                 // + journalIsCorrupt.getMessage() + ", removing");
330                 cache.delete();
331             }
332         }
333
334         // create a new empty cache
335         directory.mkdirs();
336         cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
337         cache.rebuildJournal();
338         return cache;
339     }
340
341     private void readJournal() throws IOException {
342         final InputStream in = new BufferedInputStream(new FileInputStream(journalFile),
343                 IO_BUFFER_SIZE);
344         try {
345             final String magic = readAsciiLine(in);
346             final String version = readAsciiLine(in);
347             final String appVersionString = readAsciiLine(in);
348             final String valueCountString = readAsciiLine(in);
349             final String blank = readAsciiLine(in);
350             if (!MAGIC.equals(magic) || !VERSION_1.equals(version)
351                     || !Integer.toString(appVersion).equals(appVersionString)
352                     || !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank)) {
353                 throw new IOException("unexpected journal header: [" + magic + ", " + version
354                         + ", " + valueCountString + ", " + blank + "]");
355             }
356
357             while (true) {
358                 try {
359                     readJournalLine(readAsciiLine(in));
360                 } catch (final EOFException endOfJournal) {
361                     break;
362                 }
363             }
364         } finally {
365             closeQuietly(in);
366         }
367     }
368
369     private void readJournalLine(final String line) throws IOException {
370         final String[] parts = line.split(" ");
371         if (parts.length < 2) {
372             throw new IOException("unexpected journal line: " + line);
373         }
374
375         final String key = parts[1];
376         if (parts[0].equals(REMOVE) && parts.length == 2) {
377             lruEntries.remove(key);
378             return;
379         }
380
381         Entry entry = lruEntries.get(key);
382         if (entry == null) {
383             entry = new Entry(key);
384             lruEntries.put(key, entry);
385         }
386
387         if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
388             entry.readable = true;
389             entry.currentEditor = null;
390             entry.setLengths(copyOfRange(parts, 2, parts.length));
391         } else if (parts[0].equals(DIRTY) && parts.length == 2) {
392             entry.currentEditor = new Editor(entry);
393         } else if (parts[0].equals(READ) && parts.length == 2) {
394             // this work was already done by calling lruEntries.get()
395         } else {
396             throw new IOException("unexpected journal line: " + line);
397         }
398     }
399
400     /**
401      * Computes the initial size and collects garbage as a part of opening the
402      * cache. Dirty entries are assumed to be inconsistent and will be deleted.
403      */
404     private void processJournal() throws IOException {
405         deleteIfExists(journalFileTmp);
406         for (final Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) {
407             final Entry entry = i.next();
408             if (entry.currentEditor == null) {
409                 for (int t = 0; t < valueCount; t++) {
410                     size += entry.lengths[t];
411                 }
412             } else {
413                 entry.currentEditor = null;
414                 for (int t = 0; t < valueCount; t++) {
415                     deleteIfExists(entry.getCleanFile(t));
416                     deleteIfExists(entry.getDirtyFile(t));
417                 }
418                 i.remove();
419             }
420         }
421     }
422
423     /**
424      * Creates a new journal that omits redundant information. This replaces the
425      * current journal if it exists.
426      */
427     private synchronized void rebuildJournal() throws IOException {
428         if (journalWriter != null) {
429             journalWriter.close();
430         }
431
432         final Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
433         writer.write(MAGIC);
434         writer.write("\n");
435         writer.write(VERSION_1);
436         writer.write("\n");
437         writer.write(Integer.toString(appVersion));
438         writer.write("\n");
439         writer.write(Integer.toString(valueCount));
440         writer.write("\n");
441         writer.write("\n");
442
443         for (final Entry entry : lruEntries.values()) {
444             if (entry.currentEditor != null) {
445                 writer.write(DIRTY + ' ' + entry.key + '\n');
446             } else {
447                 writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
448             }
449         }
450
451         writer.close();
452         journalFileTmp.renameTo(journalFile);
453         journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
454     }
455
456     private static void deleteIfExists(final File file) throws IOException {
457         // try {
458         // Libcore.os.remove(file.getPath());
459         // } catch (ErrnoException errnoException) {
460         // if (errnoException.errno != OsConstants.ENOENT) {
461         // throw errnoException.rethrowAsIOException();
462         // }
463         // }
464         if (file.exists() && !file.delete()) {
465             throw new IOException();
466         }
467     }
468
469     /**
470      * Returns a snapshot of the entry named {@code key}, or null if it doesn't
471      * exist is not currently readable. If a value is returned, it is moved to
472      * the head of the LRU queue.
473      */
474     public synchronized Snapshot get(final String key) throws IOException {
475         checkNotClosed();
476         validateKey(key);
477         final Entry entry = lruEntries.get(key);
478         if (entry == null) {
479             return null;
480         }
481
482         if (!entry.readable) {
483             return null;
484         }
485
486         /*
487          * Open all streams eagerly to guarantee that we see a single published
488          * snapshot. If we opened streams lazily then the streams could come
489          * from different edits.
490          */
491         final InputStream[] ins = new InputStream[valueCount];
492         try {
493             for (int i = 0; i < valueCount; i++) {
494                 ins[i] = new FileInputStream(entry.getCleanFile(i));
495             }
496         } catch (final FileNotFoundException e) {
497             // a file must have been deleted manually!
498             return null;
499         }
500
501         redundantOpCount++;
502         journalWriter.append(READ + ' ').append(key).append('\n');
503         if (journalRebuildRequired()) {
504             executorService.submit(cleanupCallable);
505         }
506
507         return new Snapshot(key, entry.sequenceNumber, ins);
508     }
509
510     /**
511      * Returns an editor for the entry named {@code key}, or null if another
512      * edit is in progress.
513      */
514     public Editor edit(final String key) throws IOException {
515         return edit(key, ANY_SEQUENCE_NUMBER);
516     }
517
518     private synchronized Editor edit(final String key, final long expectedSequenceNumber)
519             throws IOException {
520         checkNotClosed();
521         validateKey(key);
522         Entry entry = lruEntries.get(key);
523         if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
524                 && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
525             return null; // snapshot is stale
526         }
527         if (entry == null) {
528             entry = new Entry(key);
529             lruEntries.put(key, entry);
530         } else if (entry.currentEditor != null) {
531             return null; // another edit is in progress
532         }
533
534         final Editor editor = new Editor(entry);
535         entry.currentEditor = editor;
536
537         // flush the journal before creating files to prevent file leaks
538         journalWriter.write(DIRTY + ' ' + key + '\n');
539         journalWriter.flush();
540         return editor;
541     }
542
543     /**
544      * Returns the directory where this cache stores its data.
545      */
546     public File getDirectory() {
547         return directory;
548     }
549
550     /**
551      * Returns the maximum number of bytes that this cache should use to store
552      * its data.
553      */
554     public long maxSize() {
555         return maxSize;
556     }
557
558     /**
559      * Returns the number of bytes currently being used to store the values in
560      * this cache. This may be greater than the max size if a background
561      * deletion is pending.
562      */
563     public synchronized long size() {
564         return size;
565     }
566
567     private synchronized void completeEdit(final Editor editor, final boolean success)
568             throws IOException {
569         final Entry entry = editor.entry;
570         if (entry.currentEditor != editor) {
571             throw new IllegalStateException();
572         }
573
574         // if this edit is creating the entry for the first time, every index
575         // must have a value
576         if (success && !entry.readable) {
577             for (int i = 0; i < valueCount; i++) {
578                 if (!entry.getDirtyFile(i).exists()) {
579                     editor.abort();
580                     throw new IllegalStateException("edit didn't create file " + i);
581                 }
582             }
583         }
584
585         for (int i = 0; i < valueCount; i++) {
586             final File dirty = entry.getDirtyFile(i);
587             if (success) {
588                 if (dirty.exists()) {
589                     final File clean = entry.getCleanFile(i);
590                     dirty.renameTo(clean);
591                     final long oldLength = entry.lengths[i];
592                     final long newLength = clean.length();
593                     entry.lengths[i] = newLength;
594                     size = size - oldLength + newLength;
595                 }
596             } else {
597                 deleteIfExists(dirty);
598             }
599         }
600
601         redundantOpCount++;
602         entry.currentEditor = null;
603         if (entry.readable | success) {
604             entry.readable = true;
605             journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
606             if (success) {
607                 entry.sequenceNumber = nextSequenceNumber++;
608             }
609         } else {
610             lruEntries.remove(entry.key);
611             journalWriter.write(REMOVE + ' ' + entry.key + '\n');
612         }
613
614         if (size > maxSize || journalRebuildRequired()) {
615             executorService.submit(cleanupCallable);
616         }
617     }
618
619     /**
620      * We only rebuild the journal when it will halve the size of the journal
621      * and eliminate at least 2000 ops.
622      */
623     private boolean journalRebuildRequired() {
624         final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
625         return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
626                 && redundantOpCount >= lruEntries.size();
627     }
628
629     /**
630      * Drops the entry for {@code key} if it exists and can be removed. Entries
631      * actively being edited cannot be removed.
632      *
633      * @return true if an entry was removed.
634      */
635     public synchronized boolean remove(final String key) throws IOException {
636         checkNotClosed();
637         validateKey(key);
638         final Entry entry = lruEntries.get(key);
639         if (entry == null || entry.currentEditor != null) {
640             return false;
641         }
642
643         for (int i = 0; i < valueCount; i++) {
644             final File file = entry.getCleanFile(i);
645             if (!file.delete()) {
646                 throw new IOException("failed to delete " + file);
647             }
648             size -= entry.lengths[i];
649             entry.lengths[i] = 0;
650         }
651
652         redundantOpCount++;
653         journalWriter.append(REMOVE + ' ').append(key).append('\n');
654         lruEntries.remove(key);
655
656         if (journalRebuildRequired()) {
657             executorService.submit(cleanupCallable);
658         }
659
660         return true;
661     }
662
663     /**
664      * Returns true if this cache has been closed.
665      */
666     public boolean isClosed() {
667         return journalWriter == null;
668     }
669
670     private void checkNotClosed() {
671         if (journalWriter == null) {
672             throw new IllegalStateException("cache is closed");
673         }
674     }
675
676     /**
677      * Force buffered operations to the filesystem.
678      */
679     public synchronized void flush() throws IOException {
680         checkNotClosed();
681         trimToSize();
682         journalWriter.flush();
683     }
684
685     /**
686      * Closes this cache. Stored values will remain on the filesystem.
687      */
688     @Override
689     public synchronized void close() throws IOException {
690         if (journalWriter == null) {
691             return; // already closed
692         }
693         for (final Entry entry : new ArrayList<>(lruEntries.values())) {
694             if (entry.currentEditor != null) {
695                 entry.currentEditor.abort();
696             }
697         }
698         trimToSize();
699         journalWriter.close();
700         journalWriter = null;
701     }
702
703     private void trimToSize() throws IOException {
704         while (size > maxSize) {
705             // Map.Entry<String, Entry> toEvict = lruEntries.eldest();
706             final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
707             remove(toEvict.getKey());
708         }
709     }
710
711     /**
712      * Closes the cache and deletes all of its stored values. This will delete
713      * all files in the cache directory including files that weren't created by
714      * the cache.
715      */
716     public void delete() throws IOException {
717         close();
718         deleteContents(directory);
719     }
720
721     private void validateKey(final String key) {
722         if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
723             throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key
724                     + "\"");
725         }
726     }
727
728     private static String inputStreamToString(final InputStream in) throws IOException {
729         return readFully(new InputStreamReader(in, UTF_8));
730     }
731
732     /**
733      * A snapshot of the values for an entry.
734      */
735     public final class Snapshot implements Closeable {
736         private final String key;
737
738         private final long sequenceNumber;
739
740         private final InputStream[] ins;
741
742         private Snapshot(final String key, final long sequenceNumber, final InputStream[] ins) {
743             this.key = key;
744             this.sequenceNumber = sequenceNumber;
745             this.ins = ins;
746         }
747
748         /**
749          * Returns an editor for this snapshot's entry, or null if either the
750          * entry has changed since this snapshot was created or if another edit
751          * is in progress.
752          */
753         public Editor edit() throws IOException {
754             return DiskLruCache.this.edit(key, sequenceNumber);
755         }
756
757         /**
758          * Returns the unbuffered stream with the value for {@code index}.
759          */
760         public InputStream getInputStream(final int index) {
761             return ins[index];
762         }
763
764         /**
765          * Returns the string value for {@code index}.
766          */
767         public String getString(final int index) throws IOException {
768             return inputStreamToString(getInputStream(index));
769         }
770
771         @Override
772         public void close() {
773             for (final InputStream in : ins) {
774                 closeQuietly(in);
775             }
776         }
777     }
778
779     /**
780      * Edits the values for an entry.
781      */
782     public final class Editor {
783         private final Entry entry;
784
785         private boolean hasErrors;
786
787         private Editor(final Entry entry) {
788             this.entry = entry;
789         }
790
791         /**
792          * Returns an unbuffered input stream to read the last committed value,
793          * or null if no value has been committed.
794          */
795         public InputStream newInputStream(final int index) throws IOException {
796             synchronized (DiskLruCache.this) {
797                 if (entry.currentEditor != this) {
798                     throw new IllegalStateException();
799                 }
800                 if (!entry.readable) {
801                     return null;
802                 }
803                 return new FileInputStream(entry.getCleanFile(index));
804             }
805         }
806
807         /**
808          * Returns the last committed value as a string, or null if no value has
809          * been committed.
810          */
811         public String getString(final int index) throws IOException {
812             final InputStream in = newInputStream(index);
813             return in != null ? inputStreamToString(in) : null;
814         }
815
816         /**
817          * Returns a new unbuffered output stream to write the value at
818          * {@code index}. If the underlying output stream encounters errors when
819          * writing to the filesystem, this edit will be aborted when
820          * {@link #commit} is called. The returned output stream does not throw
821          * IOExceptions.
822          */
823         public OutputStream newOutputStream(final int index) throws IOException {
824             synchronized (DiskLruCache.this) {
825                 if (entry.currentEditor != this) {
826                     throw new IllegalStateException();
827                 }
828                 return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
829             }
830         }
831
832         /**
833          * Sets the value at {@code index} to {@code value}.
834          */
835         public void set(final int index, final String value) throws IOException {
836             Writer writer = null;
837             try {
838                 writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
839                 writer.write(value);
840             } finally {
841                 closeQuietly(writer);
842             }
843         }
844
845         /**
846          * Commits this edit so it is visible to readers. This releases the edit
847          * lock so another edit may be started on the same key.
848          */
849         public void commit() throws IOException {
850             if (hasErrors) {
851                 completeEdit(this, false);
852                 remove(entry.key); // the previous entry is stale
853             } else {
854                 completeEdit(this, true);
855             }
856         }
857
858         /**
859          * Aborts this edit. This releases the edit lock so another edit may be
860          * started on the same key.
861          */
862         public void abort() throws IOException {
863             completeEdit(this, false);
864         }
865
866         private class FaultHidingOutputStream extends FilterOutputStream {
867             private FaultHidingOutputStream(final OutputStream out) {
868                 super(out);
869             }
870
871             @Override
872             public void write(final int oneByte) {
873                 try {
874                     out.write(oneByte);
875                 } catch (final IOException e) {
876                     hasErrors = true;
877                 }
878             }
879
880             @Override
881             public void write(final byte[] buffer, final int offset, final int length) {
882                 try {
883                     out.write(buffer, offset, length);
884                 } catch (final IOException e) {
885                     hasErrors = true;
886                 }
887             }
888
889             @Override
890             public void close() {
891                 try {
892                     out.close();
893                 } catch (final IOException e) {
894                     hasErrors = true;
895                 }
896             }
897
898             @Override
899             public void flush() {
900                 try {
901                     out.flush();
902                 } catch (final IOException e) {
903                     hasErrors = true;
904                 }
905             }
906         }
907     }
908
909     private final class Entry {
910         private final String key;
911
912         /** Lengths of this entry's files. */
913         private final long[] lengths;
914
915         /** True if this entry has ever been published */
916         private boolean readable;
917
918         /** The ongoing edit or null if this entry is not being edited. */
919         private Editor currentEditor;
920
921         /**
922          * The sequence number of the most recently committed edit to this
923          * entry.
924          */
925         private long sequenceNumber;
926
927         private Entry(final String key) {
928             this.key = key;
929             lengths = new long[valueCount];
930         }
931
932         public String getLengths() throws IOException {
933             final StringBuilder result = new StringBuilder();
934             for (final long size : lengths) {
935                 result.append(' ').append(size);
936             }
937             return result.toString();
938         }
939
940         /**
941          * Set lengths using decimal numbers like "10123".
942          */
943         private void setLengths(final String[] strings) throws IOException {
944             if (strings.length != valueCount) {
945                 throw invalidLengths(strings);
946             }
947
948             try {
949                 for (int i = 0; i < strings.length; i++) {
950                     lengths[i] = Long.parseLong(strings[i]);
951                 }
952             } catch (final NumberFormatException e) {
953                 throw invalidLengths(strings);
954             }
955         }
956
957         private IOException invalidLengths(final String[] strings) throws IOException {
958             throw new IOException("unexpected journal line: " + Arrays.toString(strings));
959         }
960
961         public File getCleanFile(final int i) {
962             return new File(directory, key + "." + i);
963         }
964
965         public File getDirtyFile(final int i) {
966             return new File(directory, key + "." + i + ".tmp");
967         }
968     }
969 }