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 | *
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 | *
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 | *
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 | *
79 | *
When an entry is being created it is necessary to
80 | * supply a full set of values; the empty value should be used as a
81 | * placeholder if necessary.
82 | *
When an entry is being edited, it is not necessary
83 | * to supply data for every value; values default to their previous
84 | * value.
85 | *
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 | *
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 | *
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 | {
102 | static final String JOURNAL_FILE = "journal";
103 | static final String JOURNAL_FILE_TMP = "journal.tmp";
104 | static final String MAGIC = "libcore.io.DiskLruCache";
105 | static final String VERSION_1 = "1";
106 | static final long ANY_SEQUENCE_NUMBER = -1;
107 | private static final String CLEAN = "CLEAN";
108 | private static final String DIRTY = "DIRTY";
109 | private static final String REMOVE = "REMOVE";
110 | private static final String READ = "READ";
111 |
112 | private static final Charset UTF_8 = Charset.forName("UTF-8");
113 | private static final int IO_BUFFER_SIZE = 8 * 1024;
114 |
115 | /*
116 | * This cache uses a journal file named "journal". A typical journal file
117 | * looks like this:
118 | * libcore.io.DiskLruCache
119 | * 1
120 | * 100
121 | * 2
122 | *
123 | * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
124 | * DIRTY 335c4c6028171cfddfbaae1a9c313c52
125 | * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
126 | * REMOVE 335c4c6028171cfddfbaae1a9c313c52
127 | * DIRTY 1ab96a171faeeee38496d8b330771a7a
128 | * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
129 | * READ 335c4c6028171cfddfbaae1a9c313c52
130 | * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
131 | *
132 | * The first five lines of the journal form its header. They are the
133 | * constant string "libcore.io.DiskLruCache", the disk cache's version,
134 | * the application's version, the value count, and a blank line.
135 | *
136 | * Each of the subsequent lines in the file is a record of the state of a
137 | * cache entry. Each line contains space-separated values: a state, a key,
138 | * and optional state-specific values.
139 | * o DIRTY lines track that an entry is actively being created or updated.
140 | * Every successful DIRTY action should be followed by a CLEAN or REMOVE
141 | * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
142 | * temporary files may need to be deleted.
143 | * o CLEAN lines track a cache entry that has been successfully published
144 | * and may be read. A publish line is followed by the lengths of each of
145 | * its values.
146 | * o READ lines track accesses for LRU.
147 | * o REMOVE lines track entries that have been deleted.
148 | *
149 | * The journal file is appended to as cache operations occur. The journal may
150 | * occasionally be compacted by dropping redundant lines. A temporary file named
151 | * "journal.tmp" will be used during compaction; that file should be deleted if
152 | * it exists when the cache is opened.
153 | */
154 |
155 | private final File directory;
156 | private final File journalFile;
157 | private final File journalFileTmp;
158 | private final int appVersion;
159 | private final long maxSize;
160 | private final int valueCount;
161 | private long size = 0;
162 | private Writer journalWriter;
163 | private final LinkedHashMap lruEntries
164 | = new LinkedHashMap(0, 0.75f, true);
165 | private int redundantOpCount;
166 |
167 | /**
168 | * To differentiate between old and current snapshots, each entry is given
169 | * a sequence number each time an edit is committed. A snapshot is stale if
170 | * its sequence number is not equal to its entry's sequence number.
171 | */
172 | private long nextSequenceNumber = 0;
173 |
174 | /* From java.util.Arrays */
175 | @SuppressWarnings("unchecked")
176 | private static T[] copyOfRange(T[] original, int start, int end)
177 | {
178 | final int originalLength = original.length; // For exception priority compatibility.
179 | if (start > end)
180 | {
181 | throw new IllegalArgumentException();
182 | }
183 | if (start < 0 || start > originalLength)
184 | {
185 | throw new ArrayIndexOutOfBoundsException();
186 | }
187 | final int resultLength = end - start;
188 | final int copyLength = Math.min(resultLength, originalLength - start);
189 | final T[] result = (T[]) Array
190 | .newInstance(original.getClass().getComponentType(), resultLength);
191 | System.arraycopy(original, start, result, 0, copyLength);
192 | return result;
193 | }
194 |
195 | /**
196 | * Returns the remainder of 'reader' as a string, closing it when done.
197 | */
198 | public static String readFully(Reader reader) throws IOException
199 | {
200 | try
201 | {
202 | StringWriter writer = new StringWriter();
203 | char[] buffer = new char[1024];
204 | int count;
205 | while ((count = reader.read(buffer)) != -1)
206 | {
207 | writer.write(buffer, 0, count);
208 | }
209 | return writer.toString();
210 | } finally
211 | {
212 | reader.close();
213 | }
214 | }
215 |
216 | /**
217 | * Returns the ASCII characters up to but not including the next "\r\n", or
218 | * "\n".
219 | *
220 | * @throws EOFException if the stream is exhausted before the next newline
221 | * character.
222 | */
223 | public static String readAsciiLine(InputStream in) throws IOException
224 | {
225 | // TODO: support UTF-8 here instead
226 |
227 | StringBuilder result = new StringBuilder(80);
228 | while (true)
229 | {
230 | int c = in.read();
231 | if (c == -1)
232 | {
233 | throw new EOFException();
234 | } else if (c == '\n')
235 | {
236 | break;
237 | }
238 |
239 | result.append((char) c);
240 | }
241 | int length = result.length();
242 | if (length > 0 && result.charAt(length - 1) == '\r')
243 | {
244 | result.setLength(length - 1);
245 | }
246 | return result.toString();
247 | }
248 |
249 | /**
250 | * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
251 | */
252 | public static void closeQuietly(Closeable closeable)
253 | {
254 | if (closeable != null)
255 | {
256 | try
257 | {
258 | closeable.close();
259 | } catch (RuntimeException rethrown)
260 | {
261 | throw rethrown;
262 | } catch (Exception ignored)
263 | {
264 | }
265 | }
266 | }
267 |
268 | /**
269 | * Recursively delete everything in {@code dir}.
270 | */
271 | // TODO: this should specify paths as Strings rather than as Files
272 | public static void deleteContents(File dir) throws IOException
273 | {
274 | File[] files = dir.listFiles();
275 | if (files == null)
276 | {
277 | throw new IllegalArgumentException("not a directory: " + dir);
278 | }
279 | for (File file : files)
280 | {
281 | if (file.isDirectory())
282 | {
283 | deleteContents(file);
284 | }
285 | if (!file.delete())
286 | {
287 | throw new IOException("failed to delete file: " + file);
288 | }
289 | }
290 | }
291 |
292 | /**
293 | * This cache uses a single background thread to evict entries.
294 | */
295 | private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
296 | 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
297 | private final Callable cleanupCallable = new Callable()
298 | {
299 | @Override
300 | public Void call() throws Exception
301 | {
302 | synchronized (DiskLruCache.this)
303 | {
304 | if (journalWriter == null)
305 | {
306 | return null; // closed
307 | }
308 | trimToSize();
309 | if (journalRebuildRequired())
310 | {
311 | rebuildJournal();
312 | redundantOpCount = 0;
313 | }
314 | }
315 | return null;
316 | }
317 | };
318 |
319 | private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize)
320 | {
321 | this.directory = directory;
322 | this.appVersion = appVersion;
323 | this.journalFile = new File(directory, JOURNAL_FILE);
324 | this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
325 | this.valueCount = valueCount;
326 | this.maxSize = maxSize;
327 | }
328 |
329 | /**
330 | * Opens the cache in {@code directory}, creating a cache if none exists
331 | * there.
332 | *
333 | * @param directory a writable directory
334 | * @param appVersion
335 | * @param valueCount the number of values per cache entry. Must be positive.
336 | * @param maxSize the maximum number of bytes this cache should use to store
337 | * @throws IOException if reading or writing the cache directory fails
338 | */
339 | public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
340 | throws IOException
341 | {
342 | if (maxSize <= 0)
343 | {
344 | throw new IllegalArgumentException("maxSize <= 0");
345 | }
346 | if (valueCount <= 0)
347 | {
348 | throw new IllegalArgumentException("valueCount <= 0");
349 | }
350 |
351 | // prefer to pick up where we left off
352 | DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
353 | if (cache.journalFile.exists())
354 | {
355 | try
356 | {
357 | cache.readJournal();
358 | cache.processJournal();
359 | cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
360 | IO_BUFFER_SIZE);
361 | return cache;
362 | } catch (IOException journalIsCorrupt)
363 | {
364 | // System.logW("DiskLruCache " + directory + " is corrupt: "
365 | // + journalIsCorrupt.getMessage() + ", removing");
366 | cache.delete();
367 | }
368 | }
369 |
370 | // create a new empty cache
371 | directory.mkdirs();
372 | cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
373 | cache.rebuildJournal();
374 | return cache;
375 | }
376 |
377 | private void readJournal() throws IOException
378 | {
379 | InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
380 | try
381 | {
382 | String magic = readAsciiLine(in);
383 | String version = readAsciiLine(in);
384 | String appVersionString = readAsciiLine(in);
385 | String valueCountString = readAsciiLine(in);
386 | String blank = readAsciiLine(in);
387 | if (!MAGIC.equals(magic)
388 | || !VERSION_1.equals(version)
389 | || !Integer.toString(appVersion).equals(appVersionString)
390 | || !Integer.toString(valueCount).equals(valueCountString)
391 | || !"".equals(blank))
392 | {
393 | throw new IOException("unexpected journal header: ["
394 | + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
395 | }
396 |
397 | while (true)
398 | {
399 | try
400 | {
401 | readJournalLine(readAsciiLine(in));
402 | } catch (EOFException endOfJournal)
403 | {
404 | break;
405 | }
406 | }
407 | } finally
408 | {
409 | closeQuietly(in);
410 | }
411 | }
412 |
413 | private void readJournalLine(String line) throws IOException
414 | {
415 | String[] parts = line.split(" ");
416 | if (parts.length < 2)
417 | {
418 | throw new IOException("unexpected journal line: " + line);
419 | }
420 |
421 | String key = parts[1];
422 | if (parts[0].equals(REMOVE) && parts.length == 2)
423 | {
424 | lruEntries.remove(key);
425 | return;
426 | }
427 |
428 | Entry entry = lruEntries.get(key);
429 | if (entry == null)
430 | {
431 | entry = new Entry(key);
432 | lruEntries.put(key, entry);
433 | }
434 |
435 | if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount)
436 | {
437 | entry.readable = true;
438 | entry.currentEditor = null;
439 | entry.setLengths(copyOfRange(parts, 2, parts.length));
440 | } else if (parts[0].equals(DIRTY) && parts.length == 2)
441 | {
442 | entry.currentEditor = new Editor(entry);
443 | } else if (parts[0].equals(READ) && parts.length == 2)
444 | {
445 | // this work was already done by calling lruEntries.get()
446 | } else
447 | {
448 | throw new IOException("unexpected journal line: " + line);
449 | }
450 | }
451 |
452 | /**
453 | * Computes the initial size and collects garbage as a part of opening the
454 | * cache. Dirty entries are assumed to be inconsistent and will be deleted.
455 | */
456 | private void processJournal() throws IOException
457 | {
458 | deleteIfExists(journalFileTmp);
459 | for (Iterator i = lruEntries.values().iterator(); i.hasNext(); )
460 | {
461 | Entry entry = i.next();
462 | if (entry.currentEditor == null)
463 | {
464 | for (int t = 0; t < valueCount; t++)
465 | {
466 | size += entry.lengths[t];
467 | }
468 | } else
469 | {
470 | entry.currentEditor = null;
471 | for (int t = 0; t < valueCount; t++)
472 | {
473 | deleteIfExists(entry.getCleanFile(t));
474 | deleteIfExists(entry.getDirtyFile(t));
475 | }
476 | i.remove();
477 | }
478 | }
479 | }
480 |
481 | /**
482 | * Creates a new journal that omits redundant information. This replaces the
483 | * current journal if it exists.
484 | */
485 | private synchronized void rebuildJournal() throws IOException
486 | {
487 | if (journalWriter != null)
488 | {
489 | journalWriter.close();
490 | }
491 |
492 | Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
493 | writer.write(MAGIC);
494 | writer.write("\n");
495 | writer.write(VERSION_1);
496 | writer.write("\n");
497 | writer.write(Integer.toString(appVersion));
498 | writer.write("\n");
499 | writer.write(Integer.toString(valueCount));
500 | writer.write("\n");
501 | writer.write("\n");
502 |
503 | for (Entry entry : lruEntries.values())
504 | {
505 | if (entry.currentEditor != null)
506 | {
507 | writer.write(DIRTY + ' ' + entry.key + '\n');
508 | } else
509 | {
510 | writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
511 | }
512 | }
513 |
514 | writer.close();
515 | journalFileTmp.renameTo(journalFile);
516 | journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
517 | }
518 |
519 | private static void deleteIfExists(File file) throws IOException
520 | {
521 | // try {
522 | // Libcore.os.remove(file.getPath());
523 | // } catch (ErrnoException errnoException) {
524 | // if (errnoException.errno != OsConstants.ENOENT) {
525 | // throw errnoException.rethrowAsIOException();
526 | // }
527 | // }
528 | if (file.exists() && !file.delete())
529 | {
530 | throw new IOException();
531 | }
532 | }
533 |
534 | /**
535 | * Returns a snapshot of the entry named {@code key}, or null if it doesn't
536 | * exist is not currently readable. If a value is returned, it is moved to
537 | * the head of the LRU queue.
538 | */
539 | public synchronized Snapshot get(String key) throws IOException
540 | {
541 | checkNotClosed();
542 | validateKey(key);
543 | Entry entry = lruEntries.get(key);
544 | if (entry == null)
545 | {
546 | return null;
547 | }
548 |
549 | if (!entry.readable)
550 | {
551 | return null;
552 | }
553 |
554 | /*
555 | * Open all streams eagerly to guarantee that we see a single published
556 | * snapshot. If we opened streams lazily then the streams could come
557 | * from different edits.
558 | */
559 | InputStream[] ins = new InputStream[valueCount];
560 | try
561 | {
562 | for (int i = 0; i < valueCount; i++)
563 | {
564 | ins[i] = new FileInputStream(entry.getCleanFile(i));
565 | }
566 | } catch (FileNotFoundException e)
567 | {
568 | // a file must have been deleted manually!
569 | return null;
570 | }
571 |
572 | redundantOpCount++;
573 | journalWriter.append(READ + ' ' + key + '\n');
574 | if (journalRebuildRequired())
575 | {
576 | executorService.submit(cleanupCallable);
577 | }
578 |
579 | return new Snapshot(key, entry.sequenceNumber, ins);
580 | }
581 |
582 | /**
583 | * Returns an editor for the entry named {@code key}, or null if another
584 | * edit is in progress.
585 | */
586 | public Editor edit(String key) throws IOException
587 | {
588 | return edit(key, ANY_SEQUENCE_NUMBER);
589 | }
590 |
591 | private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException
592 | {
593 | checkNotClosed();
594 | validateKey(key);
595 | Entry entry = lruEntries.get(key);
596 | if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
597 | && (entry == null || entry.sequenceNumber != expectedSequenceNumber))
598 | {
599 | return null; // snapshot is stale
600 | }
601 | if (entry == null)
602 | {
603 | entry = new Entry(key);
604 | lruEntries.put(key, entry);
605 | } else if (entry.currentEditor != null)
606 | {
607 | return null; // another edit is in progress
608 | }
609 |
610 | Editor editor = new Editor(entry);
611 | entry.currentEditor = editor;
612 |
613 | // flush the journal before creating files to prevent file leaks
614 | journalWriter.write(DIRTY + ' ' + key + '\n');
615 | journalWriter.flush();
616 | return editor;
617 | }
618 |
619 | /**
620 | * Returns the directory where this cache stores its data.
621 | */
622 | public File getDirectory()
623 | {
624 | return directory;
625 | }
626 |
627 | /**
628 | * Returns the maximum number of bytes that this cache should use to store
629 | * its data.
630 | */
631 | public long maxSize()
632 | {
633 | return maxSize;
634 | }
635 |
636 | /**
637 | * Returns the number of bytes currently being used to store the values in
638 | * this cache. This may be greater than the max size if a background
639 | * deletion is pending.
640 | */
641 | public synchronized long size()
642 | {
643 | return size;
644 | }
645 |
646 | private synchronized void completeEdit(Editor editor, boolean success) throws IOException
647 | {
648 | Entry entry = editor.entry;
649 | if (entry.currentEditor != editor)
650 | {
651 | throw new IllegalStateException();
652 | }
653 |
654 | // if this edit is creating the entry for the first time, every index must have a value
655 | if (success && !entry.readable)
656 | {
657 | for (int i = 0; i < valueCount; i++)
658 | {
659 | if (!entry.getDirtyFile(i).exists())
660 | {
661 | editor.abort();
662 | throw new IllegalStateException("edit didn't create file " + i);
663 | }
664 | }
665 | }
666 |
667 | for (int i = 0; i < valueCount; i++)
668 | {
669 | File dirty = entry.getDirtyFile(i);
670 | if (success)
671 | {
672 | if (dirty.exists())
673 | {
674 | File clean = entry.getCleanFile(i);
675 | dirty.renameTo(clean);
676 | long oldLength = entry.lengths[i];
677 | long newLength = clean.length();
678 | entry.lengths[i] = newLength;
679 | size = size - oldLength + newLength;
680 | }
681 | } else
682 | {
683 | deleteIfExists(dirty);
684 | }
685 | }
686 |
687 | redundantOpCount++;
688 | entry.currentEditor = null;
689 | if (entry.readable | success)
690 | {
691 | entry.readable = true;
692 | journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
693 | if (success)
694 | {
695 | entry.sequenceNumber = nextSequenceNumber++;
696 | }
697 | } else
698 | {
699 | lruEntries.remove(entry.key);
700 | journalWriter.write(REMOVE + ' ' + entry.key + '\n');
701 | }
702 |
703 | if (size > maxSize || journalRebuildRequired())
704 | {
705 | executorService.submit(cleanupCallable);
706 | }
707 | }
708 |
709 | /**
710 | * We only rebuild the journal when it will halve the size of the journal
711 | * and eliminate at least 2000 ops.
712 | */
713 | private boolean journalRebuildRequired()
714 | {
715 | final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
716 | return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
717 | && redundantOpCount >= lruEntries.size();
718 | }
719 |
720 | /**
721 | * Drops the entry for {@code key} if it exists and can be removed. Entries
722 | * actively being edited cannot be removed.
723 | *
724 | * @return true if an entry was removed.
725 | */
726 | public synchronized boolean remove(String key) throws IOException
727 | {
728 | checkNotClosed();
729 | validateKey(key);
730 | Entry entry = lruEntries.get(key);
731 | if (entry == null || entry.currentEditor != null)
732 | {
733 | return false;
734 | }
735 |
736 | for (int i = 0; i < valueCount; i++)
737 | {
738 | File file = entry.getCleanFile(i);
739 | if (!file.delete())
740 | {
741 | throw new IOException("failed to delete " + file);
742 | }
743 | size -= entry.lengths[i];
744 | entry.lengths[i] = 0;
745 | }
746 |
747 | redundantOpCount++;
748 | journalWriter.append(REMOVE + ' ' + key + '\n');
749 | lruEntries.remove(key);
750 |
751 | if (journalRebuildRequired())
752 | {
753 | executorService.submit(cleanupCallable);
754 | }
755 |
756 | return true;
757 | }
758 |
759 | /**
760 | * Returns true if this cache has been closed.
761 | */
762 | public boolean isClosed()
763 | {
764 | return journalWriter == null;
765 | }
766 |
767 | private void checkNotClosed()
768 | {
769 | if (journalWriter == null)
770 | {
771 | throw new IllegalStateException("cache is closed");
772 | }
773 | }
774 |
775 | /**
776 | * Force buffered operations to the filesystem.
777 | */
778 | public synchronized void flush() throws IOException
779 | {
780 | checkNotClosed();
781 | trimToSize();
782 | journalWriter.flush();
783 | }
784 |
785 | /**
786 | * Closes this cache. Stored values will remain on the filesystem.
787 | */
788 | public synchronized void close() throws IOException
789 | {
790 | if (journalWriter == null)
791 | {
792 | return; // already closed
793 | }
794 | for (Entry entry : new ArrayList(lruEntries.values()))
795 | {
796 | if (entry.currentEditor != null)
797 | {
798 | entry.currentEditor.abort();
799 | }
800 | }
801 | trimToSize();
802 | journalWriter.close();
803 | journalWriter = null;
804 | }
805 |
806 | private void trimToSize() throws IOException
807 | {
808 | while (size > maxSize)
809 | {
810 | // Map.Entry toEvict = lruEntries.eldest();
811 | final Map.Entry toEvict = lruEntries.entrySet().iterator().next();
812 | remove(toEvict.getKey());
813 | }
814 | }
815 |
816 | /**
817 | * Closes the cache and deletes all of its stored values. This will delete
818 | * all files in the cache directory including files that weren't created by
819 | * the cache.
820 | */
821 | public void delete() throws IOException
822 | {
823 | close();
824 | deleteContents(directory);
825 | }
826 |
827 | private void validateKey(String key)
828 | {
829 | if (key.contains(" ") || key.contains("\n") || key.contains("\r"))
830 | {
831 | throw new IllegalArgumentException(
832 | "keys must not contain spaces or newlines: \"" + key + "\"");
833 | }
834 | }
835 |
836 | private static String inputStreamToString(InputStream in) throws IOException
837 | {
838 | return readFully(new InputStreamReader(in, UTF_8));
839 | }
840 |
841 | /**
842 | * A snapshot of the values for an entry.
843 | */
844 | public final class Snapshot implements Closeable
845 | {
846 | private final String key;
847 | private final long sequenceNumber;
848 | private final InputStream[] ins;
849 |
850 | private Snapshot(String key, long sequenceNumber, InputStream[] ins)
851 | {
852 | this.key = key;
853 | this.sequenceNumber = sequenceNumber;
854 | this.ins = ins;
855 | }
856 |
857 | /**
858 | * Returns an editor for this snapshot's entry, or null if either the
859 | * entry has changed since this snapshot was created or if another edit
860 | * is in progress.
861 | */
862 | public Editor edit() throws IOException
863 | {
864 | return DiskLruCache.this.edit(key, sequenceNumber);
865 | }
866 |
867 | /**
868 | * Returns the unbuffered stream with the value for {@code index}.
869 | */
870 | public InputStream getInputStream(int index)
871 | {
872 | return ins[index];
873 | }
874 |
875 | /**
876 | * Returns the string value for {@code index}.
877 | */
878 | public String getString(int index) throws IOException
879 | {
880 | return inputStreamToString(getInputStream(index));
881 | }
882 |
883 | @Override
884 | public void close()
885 | {
886 | for (InputStream in : ins)
887 | {
888 | closeQuietly(in);
889 | }
890 | }
891 | }
892 |
893 | /**
894 | * Edits the values for an entry.
895 | */
896 | public final class Editor
897 | {
898 | private final Entry entry;
899 | private boolean hasErrors;
900 |
901 | private Editor(Entry entry)
902 | {
903 | this.entry = entry;
904 | }
905 |
906 | /**
907 | * Returns an unbuffered input stream to read the last committed value,
908 | * or null if no value has been committed.
909 | */
910 | public InputStream newInputStream(int index) throws IOException
911 | {
912 | synchronized (DiskLruCache.this)
913 | {
914 | if (entry.currentEditor != this)
915 | {
916 | throw new IllegalStateException();
917 | }
918 | if (!entry.readable)
919 | {
920 | return null;
921 | }
922 | return new FileInputStream(entry.getCleanFile(index));
923 | }
924 | }
925 |
926 | /**
927 | * Returns the last committed value as a string, or null if no value
928 | * has been committed.
929 | */
930 | public String getString(int index) throws IOException
931 | {
932 | InputStream in = newInputStream(index);
933 | return in != null ? inputStreamToString(in) : null;
934 | }
935 |
936 | /**
937 | * Returns a new unbuffered output stream to write the value at
938 | * {@code index}. If the underlying output stream encounters errors
939 | * when writing to the filesystem, this edit will be aborted when
940 | * {@link #commit} is called. The returned output stream does not throw
941 | * IOExceptions.
942 | */
943 | public OutputStream newOutputStream(int index) throws IOException
944 | {
945 | synchronized (DiskLruCache.this)
946 | {
947 | if (entry.currentEditor != this)
948 | {
949 | throw new IllegalStateException();
950 | }
951 | return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
952 | }
953 | }
954 |
955 | /**
956 | * Sets the value at {@code index} to {@code value}.
957 | */
958 | public void set(int index, String value) throws IOException
959 | {
960 | Writer writer = null;
961 | try
962 | {
963 | writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
964 | writer.write(value);
965 | } finally
966 | {
967 | closeQuietly(writer);
968 | }
969 | }
970 |
971 | /**
972 | * Commits this edit so it is visible to readers. This releases the
973 | * edit lock so another edit may be started on the same key.
974 | */
975 | public void commit() throws IOException
976 | {
977 | if (hasErrors)
978 | {
979 | completeEdit(this, false);
980 | remove(entry.key); // the previous entry is stale
981 | } else
982 | {
983 | completeEdit(this, true);
984 | }
985 | }
986 |
987 | /**
988 | * Aborts this edit. This releases the edit lock so another edit may be
989 | * started on the same key.
990 | */
991 | public void abort() throws IOException
992 | {
993 | completeEdit(this, false);
994 | }
995 |
996 | private class FaultHidingOutputStream extends FilterOutputStream
997 | {
998 | private FaultHidingOutputStream(OutputStream out)
999 | {
1000 | super(out);
1001 | }
1002 |
1003 | @Override
1004 | public void write(int oneByte)
1005 | {
1006 | try
1007 | {
1008 | out.write(oneByte);
1009 | } catch (IOException e)
1010 | {
1011 | hasErrors = true;
1012 | }
1013 | }
1014 |
1015 | @Override
1016 | public void write(byte[] buffer, int offset, int length)
1017 | {
1018 | try
1019 | {
1020 | out.write(buffer, offset, length);
1021 | } catch (IOException e)
1022 | {
1023 | hasErrors = true;
1024 | }
1025 | }
1026 |
1027 | @Override
1028 | public void close()
1029 | {
1030 | try
1031 | {
1032 | out.close();
1033 | } catch (IOException e)
1034 | {
1035 | hasErrors = true;
1036 | }
1037 | }
1038 |
1039 | @Override
1040 | public void flush()
1041 | {
1042 | try
1043 | {
1044 | out.flush();
1045 | } catch (IOException e)
1046 | {
1047 | hasErrors = true;
1048 | }
1049 | }
1050 | }
1051 | }
1052 |
1053 | private final class Entry
1054 | {
1055 | private final String key;
1056 |
1057 | /**
1058 | * Lengths of this entry's files.
1059 | */
1060 | private final long[] lengths;
1061 |
1062 | /**
1063 | * True if this entry has ever been published
1064 | */
1065 | private boolean readable;
1066 |
1067 | /**
1068 | * The ongoing edit or null if this entry is not being edited.
1069 | */
1070 | private Editor currentEditor;
1071 |
1072 | /**
1073 | * The sequence number of the most recently committed edit to this entry.
1074 | */
1075 | private long sequenceNumber;
1076 |
1077 | private Entry(String key)
1078 | {
1079 | this.key = key;
1080 | this.lengths = new long[valueCount];
1081 | }
1082 |
1083 | public String getLengths() throws IOException
1084 | {
1085 | StringBuilder result = new StringBuilder();
1086 | for (long size : lengths)
1087 | {
1088 | result.append(' ').append(size);
1089 | }
1090 | return result.toString();
1091 | }
1092 |
1093 | /**
1094 | * Set lengths using decimal numbers like "10123".
1095 | */
1096 | private void setLengths(String[] strings) throws IOException
1097 | {
1098 | if (strings.length != valueCount)
1099 | {
1100 | throw invalidLengths(strings);
1101 | }
1102 |
1103 | try
1104 | {
1105 | for (int i = 0; i < strings.length; i++)
1106 | {
1107 | lengths[i] = Long.parseLong(strings[i]);
1108 | }
1109 | } catch (NumberFormatException e)
1110 | {
1111 | throw invalidLengths(strings);
1112 | }
1113 | }
1114 |
1115 | private IOException invalidLengths(String[] strings) throws IOException
1116 | {
1117 | throw new IOException("unexpected journal line: " + Arrays.toString(strings));
1118 | }
1119 |
1120 | public File getCleanFile(int i)
1121 | {
1122 | return new File(directory, key + "." + i);
1123 | }
1124 |
1125 | public File getDirtyFile(int i)
1126 | {
1127 | return new File(directory, key + "." + i + ".tmp");
1128 | }
1129 | }
1130 | }
--------------------------------------------------------------------------------
/cacheinterceptor/src/main/java/com/xiaolei/OkhttpCacheInterceptor/Config/Config.java:
--------------------------------------------------------------------------------
1 | package com.xiaolei.OkhttpCacheInterceptor.Config;
2 |
3 | /**
4 | * Created by xiaolei on 2017/12/9.
5 | */
6 |
7 | public class Config
8 | {
9 | public static boolean DEBUG = true;
10 | }
11 |
--------------------------------------------------------------------------------
/cacheinterceptor/src/main/java/com/xiaolei/OkhttpCacheInterceptor/Header/CacheHeaders.java:
--------------------------------------------------------------------------------
1 | package com.xiaolei.OkhttpCacheInterceptor.Header;
2 |
3 |
4 | /**
5 | * 所有请求的缓存头
6 | * Created by xiaolei on 2017/12/9.
7 | */
8 |
9 | public class CacheHeaders
10 | {
11 | // 自己设置的一个标签
12 | public static final String NORMAL = "cache:true";
13 | // 客户端可以缓存
14 | public static final String PRIVATE = "Cache-Control:private";
15 | // 客户端和代理服务器都可缓存(前端的同学,可以认为public和private是一样的)
16 | public static final String MAX_AGE = "Cache-Control:max-age=xxx";
17 | // 缓存的内容将在 xxx 秒后失效
18 | public static final String NO_CACHE = "Cache-Control:no-cache";
19 | // 需要使用对比缓存来验证缓存数据(后面介绍)
20 | public static final String PUBLIC = "Cache-Control:public";
21 | // 所有内容都不会缓存,强制缓存,对比缓存都不会触发(对于前端开发来说,缓存越多越好,so...基本上和它说886)
22 | public static final String NO_STORE = "Cache-Control:no-store";
23 | }
24 |
--------------------------------------------------------------------------------
/cacheinterceptor/src/main/java/com/xiaolei/OkhttpCacheInterceptor/Log/Log.java:
--------------------------------------------------------------------------------
1 | package com.xiaolei.OkhttpCacheInterceptor.Log;
2 |
3 |
4 | import com.xiaolei.OkhttpCacheInterceptor.Config.Config;
5 |
6 | /**
7 | * Created by xiaolei on 2017/3/9.
8 | */
9 |
10 | public class Log
11 | {
12 | public static void i(String tag,Object object)
13 | {
14 | if (Config.DEBUG) android.util.Log.i(tag, "" + object);
15 | }
16 |
17 | public static void e(String tag,Object object)
18 | {
19 | if (Config.DEBUG) android.util.Log.e(tag, "" + object);
20 | }
21 |
22 | public static void v(String tag,Object object)
23 | {
24 | if (Config.DEBUG) android.util.Log.v(tag, "" + object);
25 | }
26 |
27 | public static void w(String tag,Object object)
28 | {
29 | if (Config.DEBUG) android.util.Log.w(tag, "" + object);
30 | }
31 |
32 | public static void d(String tag,Object object)
33 | {
34 | if (Config.DEBUG) android.util.Log.d(tag, "" + object);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/cacheinterceptor/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RrtrofitCacheInterceptor
3 |
4 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## Project-wide Gradle settings.
2 | #
3 | # For more details on how to configure your build environment visit
4 | # http://www.gradle.org/docs/current/userguide/build_environment.html
5 | #
6 | # Specifies the JVM arguments used for the daemon process.
7 | # The setting is particularly useful for tweaking memory settings.
8 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
10 | #
11 | # When configured, Gradle will run in incubating parallel mode.
12 | # This option should only be used with decoupled projects. More details, visit
13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
14 | # org.gradle.parallel=true
15 | #Sat Dec 09 11:38:00 CST 2017
16 | #systemProp.https.proxyPort=1087
17 | #systemProp.http.proxyHost=127.0.0.1
18 | org.gradle.jvmargs=-Xmx1536m
19 | #systemProp.https.proxyHost=127.0.0.1
20 | #systemProp.http.proxyPort=1087
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaolei123/OkhttpCacheInterceptor/0e4094260c0198626ffbe5be3b4018451f4ace91/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Dec 09 11:37:56 CST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':cacheinterceptor'
2 |
--------------------------------------------------------------------------------