• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1  /*
2   * Copyright (C) 2011 The Android Open Source Project
3   *
4   * Licensed under the Apache License, Version 2.0 (the "License");
5   * you may not use this file except in compliance with the License.
6   * You may obtain a copy of the License at
7   *
8   *      http://www.apache.org/licenses/LICENSE-2.0
9   *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.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")
copyOfRange(T[] original, int start, int end)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       */
readFully(Reader reader)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       */
readAsciiLine(InputStream in)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       */
closeQuietly(Closeable closeable)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
deleteContents(File dir)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  
DiskLruCache(File directory, int appVersion, int valueCount, long maxSize)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       */
open(File directory, int appVersion, int valueCount, long maxSize)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  
readJournal()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  
readJournalLine(String line)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       */
processJournal()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       */
rebuildJournal()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  
deleteIfExists(File file)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       */
get(String key)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       */
edit(String key)513      public Editor edit(String key) throws IOException {
514          return edit(key, ANY_SEQUENCE_NUMBER);
515      }
516  
edit(String key, long expectedSequenceNumber)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       */
getDirectory()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       */
maxSize()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       */
size()561      public synchronized long size() {
562          return size;
563      }
564  
completeEdit(Editor editor, boolean success)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       */
journalRebuildRequired()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       */
remove(String key)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       */
isClosed()662      public boolean isClosed() {
663          return journalWriter == null;
664      }
665  
checkNotClosed()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       */
flush()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       */
close()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  
trimToSize()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       */
delete()711      public void delete() throws IOException {
712          close();
713          deleteContents(directory);
714      }
715  
validateKey(String key)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  
inputStreamToString(InputStream in)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  
Snapshot(String key, long sequenceNumber, InputStream[] ins)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           */
edit()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           */
getInputStream(int index)753          public InputStream getInputStream(int index) {
754              return ins[index];
755          }
756  
757          /**
758           * Returns the string value for {@code index}.
759           */
getString(int index)760          public String getString(int index) throws IOException {
761              return inputStreamToString(getInputStream(index));
762          }
763  
close()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  
Editor(Entry entry)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           */
newInputStream(int index)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           */
getString(int index)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           */
newOutputStream(int index)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           */
set(int index, String value)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           */
commit()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           */
abort()853          public void abort() throws IOException {
854              completeEdit(this, false);
855          }
856  
857          private class FaultHidingOutputStream extends FilterOutputStream {
FaultHidingOutputStream(OutputStream out)858              private FaultHidingOutputStream(OutputStream out) {
859                  super(out);
860              }
861  
write(int oneByte)862              @Override public void write(int oneByte) {
863                  try {
864                      out.write(oneByte);
865                  } catch (IOException e) {
866                      hasErrors = true;
867                  }
868              }
869  
write(byte[] buffer, int offset, int length)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  
close()878              @Override public void close() {
879                  try {
880                      out.close();
881                  } catch (IOException e) {
882                      hasErrors = true;
883                  }
884              }
885  
flush()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  
Entry(String key)911          private Entry(String key) {
912              this.key = key;
913              this.lengths = new long[valueCount];
914          }
915  
getLengths()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           */
setLengths(String[] strings)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  
invalidLengths(String[] strings)941          private IOException invalidLengths(String[] strings) throws IOException {
942              throw new IOException("unexpected journal line: " + Arrays.toString(strings));
943          }
944  
getCleanFile(int i)945          public File getCleanFile(int i) {
946              return new File(directory, key + "." + i);
947          }
948  
getDirtyFile(int i)949          public File getDirtyFile(int i) {
950              return new File(directory, key + "." + i + ".tmp");
951          }
952      }
953  }
954