1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.squareup.okhttp.internal; 18 19import com.squareup.okhttp.internal.io.FileSystem; 20import java.io.Closeable; 21import java.io.EOFException; 22import java.io.File; 23import java.io.FileNotFoundException; 24import java.io.IOException; 25import java.util.ArrayList; 26import java.util.Arrays; 27import java.util.Iterator; 28import java.util.LinkedHashMap; 29import java.util.NoSuchElementException; 30import java.util.concurrent.Executor; 31import java.util.concurrent.LinkedBlockingQueue; 32import java.util.concurrent.ThreadPoolExecutor; 33import java.util.concurrent.TimeUnit; 34import java.util.regex.Matcher; 35import java.util.regex.Pattern; 36import okio.Buffer; 37import okio.BufferedSink; 38import okio.BufferedSource; 39import okio.Okio; 40import okio.Sink; 41import okio.Source; 42import okio.Timeout; 43 44/** 45 * A cache that uses a bounded amount of space on a filesystem. Each cache 46 * entry has a string key and a fixed number of values. Each key must match 47 * the regex <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences, 48 * accessible as streams or files. Each value must be between {@code 0} and 49 * {@code Integer.MAX_VALUE} bytes in length. 50 * 51 * <p>The cache stores its data in a directory on the filesystem. This 52 * directory must be exclusive to the cache; the cache may delete or overwrite 53 * files from its directory. It is an error for multiple processes to use the 54 * same cache directory at the same time. 55 * 56 * <p>This cache limits the number of bytes that it will store on the 57 * filesystem. When the number of stored bytes exceeds the limit, the cache will 58 * remove entries in the background until the limit is satisfied. The limit is 59 * not strict: the cache may temporarily exceed it while waiting for files to be 60 * deleted. The limit does not include filesystem overhead or the cache 61 * journal so space-sensitive applications should set a conservative limit. 62 * 63 * <p>Clients call {@link #edit} to create or update the values of an entry. An 64 * entry may have only one editor at one time; if a value is not available to be 65 * edited then {@link #edit} will return null. 66 * <ul> 67 * <li>When an entry is being <strong>created</strong> it is necessary to 68 * supply a full set of values; the empty value should be used as a 69 * placeholder if necessary. 70 * <li>When an entry is being <strong>edited</strong>, it is not necessary 71 * to supply data for every value; values default to their previous 72 * value. 73 * </ul> 74 * Every {@link #edit} call must be matched by a call to {@link Editor#commit} 75 * or {@link Editor#abort}. Committing is atomic: a read observes the full set 76 * of values as they were before or after the commit, but never a mix of values. 77 * 78 * <p>Clients call {@link #get} to read a snapshot of an entry. The read will 79 * observe the value at the time that {@link #get} was called. Updates and 80 * removals after the call do not impact ongoing reads. 81 * 82 * <p>This class is tolerant of some I/O errors. If files are missing from the 83 * filesystem, the corresponding entries will be dropped from the cache. If 84 * an error occurs while writing a cache value, the edit will fail silently. 85 * Callers should handle other problems by catching {@code IOException} and 86 * responding appropriately. 87 */ 88public final class DiskLruCache implements Closeable { 89 static final String JOURNAL_FILE = "journal"; 90 static final String JOURNAL_FILE_TEMP = "journal.tmp"; 91 static final String JOURNAL_FILE_BACKUP = "journal.bkp"; 92 static final String MAGIC = "libcore.io.DiskLruCache"; 93 static final String VERSION_1 = "1"; 94 static final long ANY_SEQUENCE_NUMBER = -1; 95 static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,120}"); 96 private static final String CLEAN = "CLEAN"; 97 private static final String DIRTY = "DIRTY"; 98 private static final String REMOVE = "REMOVE"; 99 private static final String READ = "READ"; 100 101 /* 102 * This cache uses a journal file named "journal". A typical journal file 103 * looks like this: 104 * libcore.io.DiskLruCache 105 * 1 106 * 100 107 * 2 108 * 109 * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 110 * DIRTY 335c4c6028171cfddfbaae1a9c313c52 111 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 112 * REMOVE 335c4c6028171cfddfbaae1a9c313c52 113 * DIRTY 1ab96a171faeeee38496d8b330771a7a 114 * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 115 * READ 335c4c6028171cfddfbaae1a9c313c52 116 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 117 * 118 * The first five lines of the journal form its header. They are the 119 * constant string "libcore.io.DiskLruCache", the disk cache's version, 120 * the application's version, the value count, and a blank line. 121 * 122 * Each of the subsequent lines in the file is a record of the state of a 123 * cache entry. Each line contains space-separated values: a state, a key, 124 * and optional state-specific values. 125 * o DIRTY lines track that an entry is actively being created or updated. 126 * Every successful DIRTY action should be followed by a CLEAN or REMOVE 127 * action. DIRTY lines without a matching CLEAN or REMOVE indicate that 128 * temporary files may need to be deleted. 129 * o CLEAN lines track a cache entry that has been successfully published 130 * and may be read. A publish line is followed by the lengths of each of 131 * its values. 132 * o READ lines track accesses for LRU. 133 * o REMOVE lines track entries that have been deleted. 134 * 135 * The journal file is appended to as cache operations occur. The journal may 136 * occasionally be compacted by dropping redundant lines. A temporary file named 137 * "journal.tmp" will be used during compaction; that file should be deleted if 138 * it exists when the cache is opened. 139 */ 140 141 private final FileSystem fileSystem; 142 private final File directory; 143 private final File journalFile; 144 private final File journalFileTmp; 145 private final File journalFileBackup; 146 private final int appVersion; 147 private long maxSize; 148 private final int valueCount; 149 private long size = 0; 150 private BufferedSink journalWriter; 151 private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true); 152 private int redundantOpCount; 153 private boolean hasJournalErrors; 154 155 // Must be read and written when synchronized on 'this'. 156 private boolean initialized; 157 private boolean closed; 158 159 /** 160 * To differentiate between old and current snapshots, each entry is given 161 * a sequence number each time an edit is committed. A snapshot is stale if 162 * its sequence number is not equal to its entry's sequence number. 163 */ 164 private long nextSequenceNumber = 0; 165 166 /** Used to run 'cleanupRunnable' for journal rebuilds. */ 167 private final Executor executor; 168 private final Runnable cleanupRunnable = new Runnable() { 169 public void run() { 170 synchronized (DiskLruCache.this) { 171 if (!initialized | closed) { 172 return; // Nothing to do 173 } 174 try { 175 trimToSize(); 176 if (journalRebuildRequired()) { 177 rebuildJournal(); 178 redundantOpCount = 0; 179 } 180 } catch (IOException e) { 181 throw new RuntimeException(e); 182 } 183 } 184 } 185 }; 186 187 DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize, 188 Executor executor) { 189 this.fileSystem = fileSystem; 190 this.directory = directory; 191 this.appVersion = appVersion; 192 this.journalFile = new File(directory, JOURNAL_FILE); 193 this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); 194 this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); 195 this.valueCount = valueCount; 196 this.maxSize = maxSize; 197 this.executor = executor; 198 } 199 200 public synchronized void initialize() throws IOException { 201 assert Thread.holdsLock(this); 202 203 if (initialized) { 204 return; // Already initialized. 205 } 206 207 // If a bkp file exists, use it instead. 208 if (fileSystem.exists(journalFileBackup)) { 209 // If journal file also exists just delete backup file. 210 if (fileSystem.exists(journalFile)) { 211 fileSystem.delete(journalFileBackup); 212 } else { 213 fileSystem.rename(journalFileBackup, journalFile); 214 } 215 } 216 217 // Prefer to pick up where we left off. 218 if (fileSystem.exists(journalFile)) { 219 try { 220 readJournal(); 221 processJournal(); 222 initialized = true; 223 return; 224 } catch (IOException journalIsCorrupt) { 225 Platform.get().logW("DiskLruCache " + directory + " is corrupt: " 226 + journalIsCorrupt.getMessage() + ", removing"); 227 delete(); 228 closed = false; 229 } 230 } 231 232 rebuildJournal(); 233 234 initialized = true; 235 } 236 237 /** 238 * Create a cache which will reside in {@code directory}. This cache is lazily initialized on 239 * first access and will be created if it does not exist. 240 * 241 * @param directory a writable directory 242 * @param valueCount the number of values per cache entry. Must be positive. 243 * @param maxSize the maximum number of bytes this cache should use to store 244 */ 245 public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion, 246 int valueCount, long maxSize) { 247 if (maxSize <= 0) { 248 throw new IllegalArgumentException("maxSize <= 0"); 249 } 250 if (valueCount <= 0) { 251 throw new IllegalArgumentException("valueCount <= 0"); 252 } 253 254 // Use a single background thread to evict entries. 255 Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, 256 new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true)); 257 258 return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor); 259 } 260 261 private void readJournal() throws IOException { 262 BufferedSource source = Okio.buffer(fileSystem.source(journalFile)); 263 try { 264 String magic = source.readUtf8LineStrict(); 265 String version = source.readUtf8LineStrict(); 266 String appVersionString = source.readUtf8LineStrict(); 267 String valueCountString = source.readUtf8LineStrict(); 268 String blank = source.readUtf8LineStrict(); 269 if (!MAGIC.equals(magic) 270 || !VERSION_1.equals(version) 271 || !Integer.toString(appVersion).equals(appVersionString) 272 || !Integer.toString(valueCount).equals(valueCountString) 273 || !"".equals(blank)) { 274 throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " 275 + valueCountString + ", " + blank + "]"); 276 } 277 278 int lineCount = 0; 279 while (true) { 280 try { 281 readJournalLine(source.readUtf8LineStrict()); 282 lineCount++; 283 } catch (EOFException endOfJournal) { 284 break; 285 } 286 } 287 redundantOpCount = lineCount - lruEntries.size(); 288 289 // If we ended on a truncated line, rebuild the journal before appending to it. 290 if (!source.exhausted()) { 291 rebuildJournal(); 292 } else { 293 journalWriter = newJournalWriter(); 294 } 295 } finally { 296 Util.closeQuietly(source); 297 } 298 } 299 300 private BufferedSink newJournalWriter() throws FileNotFoundException { 301 Sink fileSink = fileSystem.appendingSink(journalFile); 302 Sink faultHidingSink = new FaultHidingSink(fileSink) { 303 @Override protected void onException(IOException e) { 304 assert (Thread.holdsLock(DiskLruCache.this)); 305 hasJournalErrors = true; 306 } 307 }; 308 return Okio.buffer(faultHidingSink); 309 } 310 311 private void readJournalLine(String line) throws IOException { 312 int firstSpace = line.indexOf(' '); 313 if (firstSpace == -1) { 314 throw new IOException("unexpected journal line: " + line); 315 } 316 317 int keyBegin = firstSpace + 1; 318 int secondSpace = line.indexOf(' ', keyBegin); 319 final String key; 320 if (secondSpace == -1) { 321 key = line.substring(keyBegin); 322 if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { 323 lruEntries.remove(key); 324 return; 325 } 326 } else { 327 key = line.substring(keyBegin, secondSpace); 328 } 329 330 Entry entry = lruEntries.get(key); 331 if (entry == null) { 332 entry = new Entry(key); 333 lruEntries.put(key, entry); 334 } 335 336 if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { 337 String[] parts = line.substring(secondSpace + 1).split(" "); 338 entry.readable = true; 339 entry.currentEditor = null; 340 entry.setLengths(parts); 341 } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { 342 entry.currentEditor = new Editor(entry); 343 } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { 344 // This work was already done by calling lruEntries.get(). 345 } else { 346 throw new IOException("unexpected journal line: " + line); 347 } 348 } 349 350 /** 351 * Computes the initial size and collects garbage as a part of opening the 352 * cache. Dirty entries are assumed to be inconsistent and will be deleted. 353 */ 354 private void processJournal() throws IOException { 355 fileSystem.delete(journalFileTmp); 356 for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) { 357 Entry entry = i.next(); 358 if (entry.currentEditor == null) { 359 for (int t = 0; t < valueCount; t++) { 360 size += entry.lengths[t]; 361 } 362 } else { 363 entry.currentEditor = null; 364 for (int t = 0; t < valueCount; t++) { 365 fileSystem.delete(entry.cleanFiles[t]); 366 fileSystem.delete(entry.dirtyFiles[t]); 367 } 368 i.remove(); 369 } 370 } 371 } 372 373 /** 374 * Creates a new journal that omits redundant information. This replaces the 375 * current journal if it exists. 376 */ 377 private synchronized void rebuildJournal() throws IOException { 378 if (journalWriter != null) { 379 journalWriter.close(); 380 } 381 382 BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp)); 383 try { 384 writer.writeUtf8(MAGIC).writeByte('\n'); 385 writer.writeUtf8(VERSION_1).writeByte('\n'); 386 writer.writeDecimalLong(appVersion).writeByte('\n'); 387 writer.writeDecimalLong(valueCount).writeByte('\n'); 388 writer.writeByte('\n'); 389 390 for (Entry entry : lruEntries.values()) { 391 if (entry.currentEditor != null) { 392 writer.writeUtf8(DIRTY).writeByte(' '); 393 writer.writeUtf8(entry.key); 394 writer.writeByte('\n'); 395 } else { 396 writer.writeUtf8(CLEAN).writeByte(' '); 397 writer.writeUtf8(entry.key); 398 entry.writeLengths(writer); 399 writer.writeByte('\n'); 400 } 401 } 402 } finally { 403 writer.close(); 404 } 405 406 if (fileSystem.exists(journalFile)) { 407 fileSystem.rename(journalFile, journalFileBackup); 408 } 409 fileSystem.rename(journalFileTmp, journalFile); 410 fileSystem.delete(journalFileBackup); 411 412 journalWriter = newJournalWriter(); 413 hasJournalErrors = false; 414 } 415 416 /** 417 * Returns a snapshot of the entry named {@code key}, or null if it doesn't 418 * exist is not currently readable. If a value is returned, it is moved to 419 * the head of the LRU queue. 420 */ 421 public synchronized Snapshot get(String key) throws IOException { 422 initialize(); 423 424 checkNotClosed(); 425 validateKey(key); 426 Entry entry = lruEntries.get(key); 427 if (entry == null || !entry.readable) return null; 428 429 Snapshot snapshot = entry.snapshot(); 430 if (snapshot == null) return null; 431 432 redundantOpCount++; 433 journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n'); 434 if (journalRebuildRequired()) { 435 executor.execute(cleanupRunnable); 436 } 437 438 return snapshot; 439 } 440 441 /** 442 * Returns an editor for the entry named {@code key}, or null if another 443 * edit is in progress. 444 */ 445 public Editor edit(String key) throws IOException { 446 return edit(key, ANY_SEQUENCE_NUMBER); 447 } 448 449 private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { 450 initialize(); 451 452 checkNotClosed(); 453 validateKey(key); 454 Entry entry = lruEntries.get(key); 455 if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null 456 || entry.sequenceNumber != expectedSequenceNumber)) { 457 return null; // Snapshot is stale. 458 } 459 if (entry != null && entry.currentEditor != null) { 460 return null; // Another edit is in progress. 461 } 462 463 // Flush the journal before creating files to prevent file leaks. 464 journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n'); 465 journalWriter.flush(); 466 467 if (hasJournalErrors) { 468 return null; // Don't edit; the journal can't be written. 469 } 470 471 if (entry == null) { 472 entry = new Entry(key); 473 lruEntries.put(key, entry); 474 } 475 Editor editor = new Editor(entry); 476 entry.currentEditor = editor; 477 return editor; 478 } 479 480 /** Returns the directory where this cache stores its data. */ 481 public File getDirectory() { 482 return directory; 483 } 484 485 /** 486 * Returns the maximum number of bytes that this cache should use to store 487 * its data. 488 */ 489 public synchronized long getMaxSize() { 490 return maxSize; 491 } 492 493 /** 494 * Changes the maximum number of bytes the cache can store and queues a job 495 * to trim the existing store, if necessary. 496 */ 497 public synchronized void setMaxSize(long maxSize) { 498 this.maxSize = maxSize; 499 if (initialized) { 500 executor.execute(cleanupRunnable); 501 } 502 } 503 504 /** 505 * Returns the number of bytes currently being used to store the values in 506 * this cache. This may be greater than the max size if a background 507 * deletion is pending. 508 */ 509 public synchronized long size() throws IOException { 510 initialize(); 511 return size; 512 } 513 514 private synchronized void completeEdit(Editor editor, boolean success) throws IOException { 515 Entry entry = editor.entry; 516 if (entry.currentEditor != editor) { 517 throw new IllegalStateException(); 518 } 519 520 // If this edit is creating the entry for the first time, every index must have a value. 521 if (success && !entry.readable) { 522 for (int i = 0; i < valueCount; i++) { 523 if (!editor.written[i]) { 524 editor.abort(); 525 throw new IllegalStateException("Newly created entry didn't create value for index " + i); 526 } 527 if (!fileSystem.exists(entry.dirtyFiles[i])) { 528 editor.abort(); 529 return; 530 } 531 } 532 } 533 534 for (int i = 0; i < valueCount; i++) { 535 File dirty = entry.dirtyFiles[i]; 536 if (success) { 537 if (fileSystem.exists(dirty)) { 538 File clean = entry.cleanFiles[i]; 539 fileSystem.rename(dirty, clean); 540 long oldLength = entry.lengths[i]; 541 long newLength = fileSystem.size(clean); 542 entry.lengths[i] = newLength; 543 size = size - oldLength + newLength; 544 } 545 } else { 546 fileSystem.delete(dirty); 547 } 548 } 549 550 redundantOpCount++; 551 entry.currentEditor = null; 552 if (entry.readable | success) { 553 entry.readable = true; 554 journalWriter.writeUtf8(CLEAN).writeByte(' '); 555 journalWriter.writeUtf8(entry.key); 556 entry.writeLengths(journalWriter); 557 journalWriter.writeByte('\n'); 558 if (success) { 559 entry.sequenceNumber = nextSequenceNumber++; 560 } 561 } else { 562 lruEntries.remove(entry.key); 563 journalWriter.writeUtf8(REMOVE).writeByte(' '); 564 journalWriter.writeUtf8(entry.key); 565 journalWriter.writeByte('\n'); 566 } 567 journalWriter.flush(); 568 569 if (size > maxSize || journalRebuildRequired()) { 570 executor.execute(cleanupRunnable); 571 } 572 } 573 574 /** 575 * We only rebuild the journal when it will halve the size of the journal 576 * and eliminate at least 2000 ops. 577 */ 578 private boolean journalRebuildRequired() { 579 final int redundantOpCompactThreshold = 2000; 580 return redundantOpCount >= redundantOpCompactThreshold 581 && redundantOpCount >= lruEntries.size(); 582 } 583 584 /** 585 * Drops the entry for {@code key} if it exists and can be removed. If the 586 * entry for {@code key} is currently being edited, that edit will complete 587 * normally but its value will not be stored. 588 * 589 * @return true if an entry was removed. 590 */ 591 public synchronized boolean remove(String key) throws IOException { 592 initialize(); 593 594 checkNotClosed(); 595 validateKey(key); 596 Entry entry = lruEntries.get(key); 597 if (entry == null) return false; 598 return removeEntry(entry); 599 } 600 601 private boolean removeEntry(Entry entry) throws IOException { 602 if (entry.currentEditor != null) { 603 entry.currentEditor.detach(); // Prevent the edit from completing normally. 604 } 605 606 for (int i = 0; i < valueCount; i++) { 607 fileSystem.delete(entry.cleanFiles[i]); 608 size -= entry.lengths[i]; 609 entry.lengths[i] = 0; 610 } 611 612 redundantOpCount++; 613 journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n'); 614 lruEntries.remove(entry.key); 615 616 if (journalRebuildRequired()) { 617 executor.execute(cleanupRunnable); 618 } 619 620 return true; 621 } 622 623 /** Returns true if this cache has been closed. */ 624 public synchronized boolean isClosed() { 625 return closed; 626 } 627 628 private synchronized void checkNotClosed() { 629 if (isClosed()) { 630 throw new IllegalStateException("cache is closed"); 631 } 632 } 633 634 /** Force buffered operations to the filesystem. */ 635 public synchronized void flush() throws IOException { 636 if (!initialized) return; 637 638 checkNotClosed(); 639 trimToSize(); 640 journalWriter.flush(); 641 } 642 643 /** Closes this cache. Stored values will remain on the filesystem. */ 644 public synchronized void close() throws IOException { 645 if (!initialized || closed) { 646 closed = true; 647 return; 648 } 649 // Copying for safe iteration. 650 for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) { 651 if (entry.currentEditor != null) { 652 entry.currentEditor.abort(); 653 } 654 } 655 trimToSize(); 656 journalWriter.close(); 657 journalWriter = null; 658 closed = true; 659 } 660 661 private void trimToSize() throws IOException { 662 while (size > maxSize) { 663 Entry toEvict = lruEntries.values().iterator().next(); 664 removeEntry(toEvict); 665 } 666 } 667 668 /** 669 * Closes the cache and deletes all of its stored values. This will delete 670 * all files in the cache directory including files that weren't created by 671 * the cache. 672 */ 673 public void delete() throws IOException { 674 close(); 675 fileSystem.deleteContents(directory); 676 } 677 678 /** 679 * Deletes all stored values from the cache. In-flight edits will complete 680 * normally but their values will not be stored. 681 */ 682 public synchronized void evictAll() throws IOException { 683 initialize(); 684 // Copying for safe iteration. 685 for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) { 686 removeEntry(entry); 687 } 688 } 689 690 private void validateKey(String key) { 691 Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); 692 if (!matcher.matches()) { 693 throw new IllegalArgumentException( 694 "keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\""); 695 } 696 } 697 698 /** 699 * Returns an iterator over the cache's current entries. This iterator doesn't throw {@code 700 * ConcurrentModificationException}, but if new entries are added while iterating, those new 701 * entries will not be returned by the iterator. If existing entries are removed during iteration, 702 * they will be absent (unless they were already returned). 703 * 704 * <p>If there are I/O problems during iteration, this iterator fails silently. For example, if 705 * the hosting filesystem becomes unreachable, the iterator will omit elements rather than 706 * throwing exceptions. 707 * 708 * <p><strong>The caller must {@link Snapshot#close close}</strong> each snapshot returned by 709 * {@link Iterator#next}. Failing to do so leaks open files! 710 * 711 * <p>The returned iterator supports {@link Iterator#remove}. 712 */ 713 public synchronized Iterator<Snapshot> snapshots() throws IOException { 714 initialize(); 715 return new Iterator<Snapshot>() { 716 /** Iterate a copy of the entries to defend against concurrent modification errors. */ 717 final Iterator<Entry> delegate = new ArrayList<>(lruEntries.values()).iterator(); 718 719 /** The snapshot to return from {@link #next}. Null if we haven't computed that yet. */ 720 Snapshot nextSnapshot; 721 722 /** The snapshot to remove with {@link #remove}. Null if removal is illegal. */ 723 Snapshot removeSnapshot; 724 725 @Override public boolean hasNext() { 726 if (nextSnapshot != null) return true; 727 728 synchronized (DiskLruCache.this) { 729 // If the cache is closed, truncate the iterator. 730 if (closed) return false; 731 732 while (delegate.hasNext()) { 733 Entry entry = delegate.next(); 734 Snapshot snapshot = entry.snapshot(); 735 if (snapshot == null) continue; // Evicted since we copied the entries. 736 nextSnapshot = snapshot; 737 return true; 738 } 739 } 740 741 return false; 742 } 743 744 @Override public Snapshot next() { 745 if (!hasNext()) throw new NoSuchElementException(); 746 removeSnapshot = nextSnapshot; 747 nextSnapshot = null; 748 return removeSnapshot; 749 } 750 751 @Override public void remove() { 752 if (removeSnapshot == null) throw new IllegalStateException("remove() before next()"); 753 try { 754 DiskLruCache.this.remove(removeSnapshot.key); 755 } catch (IOException ignored) { 756 // Nothing useful to do here. We failed to remove from the cache. Most likely that's 757 // because we couldn't update the journal, but the cached entry will still be gone. 758 } finally { 759 removeSnapshot = null; 760 } 761 } 762 }; 763 } 764 765 /** A snapshot of the values for an entry. */ 766 public final class Snapshot implements Closeable { 767 private final String key; 768 private final long sequenceNumber; 769 private final Source[] sources; 770 private final long[] lengths; 771 772 private Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) { 773 this.key = key; 774 this.sequenceNumber = sequenceNumber; 775 this.sources = sources; 776 this.lengths = lengths; 777 } 778 779 public String key() { 780 return key; 781 } 782 783 /** 784 * Returns an editor for this snapshot's entry, or null if either the 785 * entry has changed since this snapshot was created or if another edit 786 * is in progress. 787 */ 788 public Editor edit() throws IOException { 789 return DiskLruCache.this.edit(key, sequenceNumber); 790 } 791 792 /** Returns the unbuffered stream with the value for {@code index}. */ 793 public Source getSource(int index) { 794 return sources[index]; 795 } 796 797 /** Returns the byte length of the value for {@code index}. */ 798 public long getLength(int index) { 799 return lengths[index]; 800 } 801 802 public void close() { 803 for (Source in : sources) { 804 Util.closeQuietly(in); 805 } 806 } 807 } 808 809 private static final Sink NULL_SINK = new Sink() { 810 @Override public void write(Buffer source, long byteCount) throws IOException { 811 source.skip(byteCount); 812 } 813 814 @Override public void flush() throws IOException { 815 } 816 817 @Override public Timeout timeout() { 818 return Timeout.NONE; 819 } 820 821 @Override public void close() throws IOException { 822 } 823 }; 824 825 /** Edits the values for an entry. */ 826 public final class Editor { 827 private final Entry entry; 828 private final boolean[] written; 829 private boolean done; 830 831 private Editor(Entry entry) { 832 this.entry = entry; 833 this.written = (entry.readable) ? null : new boolean[valueCount]; 834 } 835 836 /** 837 * Prevents this editor from completing normally. This is necessary either when the edit causes 838 * an I/O error, or if the target entry is evicted while this editor is active. In either case 839 * we delete the editor's created files and prevent new files from being created. Note that once 840 * an editor has been detached it is possible for another editor to edit the entry. 841 */ 842 void detach() { 843 if (entry.currentEditor == this) { 844 for (int i = 0; i < valueCount; i++) { 845 try { 846 fileSystem.delete(entry.dirtyFiles[i]); 847 } catch (IOException e) { 848 // This file is potentially leaked. Not much we can do about that. 849 } 850 } 851 entry.currentEditor = null; 852 } 853 } 854 855 /** 856 * Returns an unbuffered input stream to read the last committed value, 857 * or null if no value has been committed. 858 */ 859 public Source newSource(int index) throws IOException { 860 synchronized (DiskLruCache.this) { 861 if (done) { 862 throw new IllegalStateException(); 863 } 864 if (!entry.readable || entry.currentEditor != this) { 865 return null; 866 } 867 try { 868 return fileSystem.source(entry.cleanFiles[index]); 869 } catch (FileNotFoundException e) { 870 return null; 871 } 872 } 873 } 874 875 /** 876 * Returns a new unbuffered output stream to write the value at 877 * {@code index}. If the underlying output stream encounters errors 878 * when writing to the filesystem, this edit will be aborted when 879 * {@link #commit} is called. The returned output stream does not throw 880 * IOExceptions. 881 */ 882 public Sink newSink(int index) throws IOException { 883 synchronized (DiskLruCache.this) { 884 if (done) { 885 throw new IllegalStateException(); 886 } 887 if (entry.currentEditor != this) { 888 return NULL_SINK; 889 } 890 if (!entry.readable) { 891 written[index] = true; 892 } 893 File dirtyFile = entry.dirtyFiles[index]; 894 Sink sink; 895 try { 896 sink = fileSystem.sink(dirtyFile); 897 } catch (FileNotFoundException e) { 898 return NULL_SINK; 899 } 900 return new FaultHidingSink(sink) { 901 @Override protected void onException(IOException e) { 902 synchronized (DiskLruCache.this) { 903 detach(); 904 } 905 } 906 }; 907 } 908 } 909 910 /** 911 * Commits this edit so it is visible to readers. This releases the 912 * edit lock so another edit may be started on the same key. 913 */ 914 public void commit() throws IOException { 915 synchronized (DiskLruCache.this) { 916 if (done) { 917 throw new IllegalStateException(); 918 } 919 if (entry.currentEditor == this) { 920 completeEdit(this, true); 921 } 922 done = true; 923 } 924 } 925 926 /** 927 * Aborts this edit. This releases the edit lock so another edit may be 928 * started on the same key. 929 */ 930 public void abort() throws IOException { 931 synchronized (DiskLruCache.this) { 932 if (done) { 933 throw new IllegalStateException(); 934 } 935 if (entry.currentEditor == this) { 936 completeEdit(this, false); 937 } 938 done = true; 939 } 940 } 941 942 public void abortUnlessCommitted() { 943 synchronized (DiskLruCache.this) { 944 if (!done && entry.currentEditor == this) { 945 try { 946 completeEdit(this, false); 947 } catch (IOException ignored) { 948 } 949 } 950 } 951 } 952 } 953 954 private final class Entry { 955 private final String key; 956 957 /** Lengths of this entry's files. */ 958 private final long[] lengths; 959 private final File[] cleanFiles; 960 private final File[] dirtyFiles; 961 962 /** True if this entry has ever been published. */ 963 private boolean readable; 964 965 /** The ongoing edit or null if this entry is not being edited. */ 966 private Editor currentEditor; 967 968 /** The sequence number of the most recently committed edit to this entry. */ 969 private long sequenceNumber; 970 971 private Entry(String key) { 972 this.key = key; 973 974 lengths = new long[valueCount]; 975 cleanFiles = new File[valueCount]; 976 dirtyFiles = new File[valueCount]; 977 978 // The names are repetitive so re-use the same builder to avoid allocations. 979 StringBuilder fileBuilder = new StringBuilder(key).append('.'); 980 int truncateTo = fileBuilder.length(); 981 for (int i = 0; i < valueCount; i++) { 982 fileBuilder.append(i); 983 cleanFiles[i] = new File(directory, fileBuilder.toString()); 984 fileBuilder.append(".tmp"); 985 dirtyFiles[i] = new File(directory, fileBuilder.toString()); 986 fileBuilder.setLength(truncateTo); 987 } 988 } 989 990 /** Set lengths using decimal numbers like "10123". */ 991 private void setLengths(String[] strings) throws IOException { 992 if (strings.length != valueCount) { 993 throw invalidLengths(strings); 994 } 995 996 try { 997 for (int i = 0; i < strings.length; i++) { 998 lengths[i] = Long.parseLong(strings[i]); 999 } 1000 } catch (NumberFormatException e) { 1001 throw invalidLengths(strings); 1002 } 1003 } 1004 1005 /** Append space-prefixed lengths to {@code writer}. */ 1006 void writeLengths(BufferedSink writer) throws IOException { 1007 for (long length : lengths) { 1008 writer.writeByte(' ').writeDecimalLong(length); 1009 } 1010 } 1011 1012 private IOException invalidLengths(String[] strings) throws IOException { 1013 throw new IOException("unexpected journal line: " + Arrays.toString(strings)); 1014 } 1015 1016 /** 1017 * Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a 1018 * single published snapshot. If we opened streams lazily then the streams could come from 1019 * different edits. 1020 */ 1021 Snapshot snapshot() { 1022 if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError(); 1023 1024 Source[] sources = new Source[valueCount]; 1025 long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out. 1026 try { 1027 for (int i = 0; i < valueCount; i++) { 1028 sources[i] = fileSystem.source(cleanFiles[i]); 1029 } 1030 return new Snapshot(key, sequenceNumber, sources, lengths); 1031 } catch (FileNotFoundException e) { 1032 // A file must have been deleted manually! 1033 for (int i = 0; i < valueCount; i++) { 1034 if (sources[i] != null) { 1035 Util.closeQuietly(sources[i]); 1036 } else { 1037 break; 1038 } 1039 } 1040 return null; 1041 } 1042 } 1043 } 1044} 1045