Home | History | Annotate | Download | only in util
      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.example.android.displayingbitmaps.util;
     18 
     19 import java.io.BufferedInputStream;
     20 import java.io.BufferedWriter;
     21 import java.io.Closeable;
     22 import java.io.EOFException;
     23 import java.io.File;
     24 import java.io.FileInputStream;
     25 import java.io.FileNotFoundException;
     26 import java.io.FileOutputStream;
     27 import java.io.FileWriter;
     28 import java.io.FilterOutputStream;
     29 import java.io.IOException;
     30 import java.io.InputStream;
     31 import java.io.InputStreamReader;
     32 import java.io.OutputStream;
     33 import java.io.OutputStreamWriter;
     34 import java.io.Reader;
     35 import java.io.StringWriter;
     36 import java.io.Writer;
     37 import java.lang.reflect.Array;
     38 import java.nio.charset.Charset;
     39 import java.util.ArrayList;
     40 import java.util.Arrays;
     41 import java.util.Iterator;
     42 import java.util.LinkedHashMap;
     43 import java.util.Map;
     44 import java.util.concurrent.Callable;
     45 import java.util.concurrent.ExecutorService;
     46 import java.util.concurrent.LinkedBlockingQueue;
     47 import java.util.concurrent.ThreadPoolExecutor;
     48 import 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  */
    100 public 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