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