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 java.io.BufferedWriter; 20import java.io.Closeable; 21import java.io.EOFException; 22import java.io.File; 23import java.io.FileInputStream; 24import java.io.FileNotFoundException; 25import java.io.FileOutputStream; 26import java.io.FilterOutputStream; 27import java.io.IOException; 28import java.io.InputStream; 29import java.io.InputStreamReader; 30import java.io.OutputStream; 31import java.io.OutputStreamWriter; 32import java.io.Writer; 33import java.util.ArrayList; 34import java.util.Iterator; 35import java.util.LinkedHashMap; 36import java.util.Map; 37import java.util.concurrent.Callable; 38import java.util.concurrent.LinkedBlockingQueue; 39import java.util.concurrent.ThreadPoolExecutor; 40import java.util.concurrent.TimeUnit; 41import java.util.regex.Matcher; 42import java.util.regex.Pattern; 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,64}"); 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 File directory; 142 private final File journalFile; 143 private final File journalFileTmp; 144 private final File journalFileBackup; 145 private final int appVersion; 146 private long maxSize; 147 private final int valueCount; 148 private long size = 0; 149 private Writer journalWriter; 150 private final LinkedHashMap<String, Entry> lruEntries = 151 new LinkedHashMap<String, Entry>(0, 0.75f, true); 152 private int redundantOpCount; 153 154 /** 155 * To differentiate between old and current snapshots, each entry is given 156 * a sequence number each time an edit is committed. A snapshot is stale if 157 * its sequence number is not equal to its entry's sequence number. 158 */ 159 private long nextSequenceNumber = 0; 160 161 /** This cache uses a single background thread to evict entries. */ 162 final ThreadPoolExecutor executorService = 163 new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); 164 private final Callable<Void> cleanupCallable = new Callable<Void>() { 165 public Void call() throws Exception { 166 synchronized (DiskLruCache.this) { 167 if (journalWriter == null) { 168 return null; // Closed. 169 } 170 trimToSize(); 171 if (journalRebuildRequired()) { 172 rebuildJournal(); 173 redundantOpCount = 0; 174 } 175 } 176 return null; 177 } 178 }; 179 180 private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { 181 this.directory = directory; 182 this.appVersion = appVersion; 183 this.journalFile = new File(directory, JOURNAL_FILE); 184 this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); 185 this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); 186 this.valueCount = valueCount; 187 this.maxSize = maxSize; 188 } 189 190 /** 191 * Opens the cache in {@code directory}, creating a cache if none exists 192 * there. 193 * 194 * @param directory a writable directory 195 * @param valueCount the number of values per cache entry. Must be positive. 196 * @param maxSize the maximum number of bytes this cache should use to store 197 * @throws IOException if reading or writing the cache directory fails 198 */ 199 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 200 throws IOException { 201 if (maxSize <= 0) { 202 throw new IllegalArgumentException("maxSize <= 0"); 203 } 204 if (valueCount <= 0) { 205 throw new IllegalArgumentException("valueCount <= 0"); 206 } 207 208 // If a bkp file exists, use it instead. 209 File backupFile = new File(directory, JOURNAL_FILE_BACKUP); 210 if (backupFile.exists()) { 211 File journalFile = new File(directory, JOURNAL_FILE); 212 // If journal file also exists just delete backup file. 213 if (journalFile.exists()) { 214 backupFile.delete(); 215 } else { 216 renameTo(backupFile, journalFile, false); 217 } 218 } 219 220 // Prefer to pick up where we left off. 221 DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 222 if (cache.journalFile.exists()) { 223 try { 224 cache.readJournal(); 225 cache.processJournal(); 226 cache.journalWriter = new BufferedWriter( 227 new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII)); 228 return cache; 229 } catch (IOException journalIsCorrupt) { 230 Platform.get().logW("DiskLruCache " + directory + " is corrupt: " 231 + journalIsCorrupt.getMessage() + ", removing"); 232 cache.delete(); 233 } 234 } 235 236 // Create a new empty cache. 237 directory.mkdirs(); 238 cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 239 cache.rebuildJournal(); 240 return cache; 241 } 242 243 private void readJournal() throws IOException { 244 StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); 245 try { 246 String magic = reader.readLine(); 247 String version = reader.readLine(); 248 String appVersionString = reader.readLine(); 249 String valueCountString = reader.readLine(); 250 String blank = reader.readLine(); 251 if (!MAGIC.equals(magic) 252 || !VERSION_1.equals(version) 253 || !Integer.toString(appVersion).equals(appVersionString) 254 || !Integer.toString(valueCount).equals(valueCountString) 255 || !"".equals(blank)) { 256 throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " 257 + valueCountString + ", " + blank + "]"); 258 } 259 260 int lineCount = 0; 261 while (true) { 262 try { 263 readJournalLine(reader.readLine()); 264 lineCount++; 265 } catch (EOFException endOfJournal) { 266 break; 267 } 268 } 269 redundantOpCount = lineCount - lruEntries.size(); 270 } finally { 271 Util.closeQuietly(reader); 272 } 273 } 274 275 private void readJournalLine(String line) throws IOException { 276 int firstSpace = line.indexOf(' '); 277 if (firstSpace == -1) { 278 throw new IOException("unexpected journal line: " + line); 279 } 280 281 int keyBegin = firstSpace + 1; 282 int secondSpace = line.indexOf(' ', keyBegin); 283 final String key; 284 if (secondSpace == -1) { 285 key = line.substring(keyBegin); 286 if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { 287 lruEntries.remove(key); 288 return; 289 } 290 } else { 291 key = line.substring(keyBegin, secondSpace); 292 } 293 294 Entry entry = lruEntries.get(key); 295 if (entry == null) { 296 entry = new Entry(key); 297 lruEntries.put(key, entry); 298 } 299 300 if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { 301 String[] parts = line.substring(secondSpace + 1).split(" "); 302 entry.readable = true; 303 entry.currentEditor = null; 304 entry.setLengths(parts); 305 } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { 306 entry.currentEditor = new Editor(entry); 307 } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { 308 // This work was already done by calling lruEntries.get(). 309 } else { 310 throw new IOException("unexpected journal line: " + line); 311 } 312 } 313 314 /** 315 * Computes the initial size and collects garbage as a part of opening the 316 * cache. Dirty entries are assumed to be inconsistent and will be deleted. 317 */ 318 private void processJournal() throws IOException { 319 deleteIfExists(journalFileTmp); 320 for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) { 321 Entry entry = i.next(); 322 if (entry.currentEditor == null) { 323 for (int t = 0; t < valueCount; t++) { 324 size += entry.lengths[t]; 325 } 326 } else { 327 entry.currentEditor = null; 328 for (int t = 0; t < valueCount; t++) { 329 deleteIfExists(entry.getCleanFile(t)); 330 deleteIfExists(entry.getDirtyFile(t)); 331 } 332 i.remove(); 333 } 334 } 335 } 336 337 /** 338 * Creates a new journal that omits redundant information. This replaces the 339 * current journal if it exists. 340 */ 341 private synchronized void rebuildJournal() throws IOException { 342 if (journalWriter != null) { 343 journalWriter.close(); 344 } 345 346 Writer writer = new BufferedWriter( 347 new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); 348 try { 349 writer.write(MAGIC); 350 writer.write("\n"); 351 writer.write(VERSION_1); 352 writer.write("\n"); 353 writer.write(Integer.toString(appVersion)); 354 writer.write("\n"); 355 writer.write(Integer.toString(valueCount)); 356 writer.write("\n"); 357 writer.write("\n"); 358 359 for (Entry entry : lruEntries.values()) { 360 if (entry.currentEditor != null) { 361 writer.write(DIRTY + ' ' + entry.key + '\n'); 362 } else { 363 writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 364 } 365 } 366 } finally { 367 writer.close(); 368 } 369 370 if (journalFile.exists()) { 371 renameTo(journalFile, journalFileBackup, true); 372 } 373 renameTo(journalFileTmp, journalFile, false); 374 journalFileBackup.delete(); 375 376 journalWriter = new BufferedWriter( 377 new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); 378 } 379 380 private static void deleteIfExists(File file) throws IOException { 381 if (file.exists() && !file.delete()) { 382 throw new IOException(); 383 } 384 } 385 386 private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { 387 if (deleteDestination) { 388 deleteIfExists(to); 389 } 390 if (!from.renameTo(to)) { 391 throw new IOException(); 392 } 393 } 394 395 /** 396 * Returns a snapshot of the entry named {@code key}, or null if it doesn't 397 * exist is not currently readable. If a value is returned, it is moved to 398 * the head of the LRU queue. 399 */ 400 public synchronized Snapshot get(String key) throws IOException { 401 checkNotClosed(); 402 validateKey(key); 403 Entry entry = lruEntries.get(key); 404 if (entry == null) { 405 return null; 406 } 407 408 if (!entry.readable) { 409 return null; 410 } 411 412 // Open all streams eagerly to guarantee that we see a single published 413 // snapshot. If we opened streams lazily then the streams could come 414 // from different edits. 415 InputStream[] ins = new InputStream[valueCount]; 416 try { 417 for (int i = 0; i < valueCount; i++) { 418 ins[i] = new FileInputStream(entry.getCleanFile(i)); 419 } 420 } catch (FileNotFoundException e) { 421 // A file must have been deleted manually! 422 for (int i = 0; i < valueCount; i++) { 423 if (ins[i] != null) { 424 Util.closeQuietly(ins[i]); 425 } else { 426 break; 427 } 428 } 429 return null; 430 } 431 432 redundantOpCount++; 433 journalWriter.append(READ + ' ' + key + '\n'); 434 if (journalRebuildRequired()) { 435 executorService.submit(cleanupCallable); 436 } 437 438 return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); 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 checkNotClosed(); 451 validateKey(key); 452 Entry entry = lruEntries.get(key); 453 if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null 454 || entry.sequenceNumber != expectedSequenceNumber)) { 455 return null; // Snapshot is stale. 456 } 457 if (entry == null) { 458 entry = new Entry(key); 459 lruEntries.put(key, entry); 460 } else if (entry.currentEditor != null) { 461 return null; // Another edit is in progress. 462 } 463 464 Editor editor = new Editor(entry); 465 entry.currentEditor = editor; 466 467 // Flush the journal before creating files to prevent file leaks. 468 journalWriter.write(DIRTY + ' ' + key + '\n'); 469 journalWriter.flush(); 470 return editor; 471 } 472 473 /** Returns the directory where this cache stores its data. */ 474 public File getDirectory() { 475 return directory; 476 } 477 478 /** 479 * Returns the maximum number of bytes that this cache should use to store 480 * its data. 481 */ 482 public long getMaxSize() { 483 return maxSize; 484 } 485 486 /** 487 * Changes the maximum number of bytes the cache can store and queues a job 488 * to trim the existing store, if necessary. 489 */ 490 public synchronized void setMaxSize(long maxSize) { 491 this.maxSize = maxSize; 492 executorService.submit(cleanupCallable); 493 } 494 495 /** 496 * Returns the number of bytes currently being used to store the values in 497 * this cache. This may be greater than the max size if a background 498 * deletion is pending. 499 */ 500 public synchronized long size() { 501 return size; 502 } 503 504 private synchronized void completeEdit(Editor editor, boolean success) throws IOException { 505 Entry entry = editor.entry; 506 if (entry.currentEditor != editor) { 507 throw new IllegalStateException(); 508 } 509 510 // If this edit is creating the entry for the first time, every index must have a value. 511 if (success && !entry.readable) { 512 for (int i = 0; i < valueCount; i++) { 513 if (!editor.written[i]) { 514 editor.abort(); 515 throw new IllegalStateException("Newly created entry didn't create value for index " + i); 516 } 517 if (!entry.getDirtyFile(i).exists()) { 518 editor.abort(); 519 return; 520 } 521 } 522 } 523 524 for (int i = 0; i < valueCount; i++) { 525 File dirty = entry.getDirtyFile(i); 526 if (success) { 527 if (dirty.exists()) { 528 File clean = entry.getCleanFile(i); 529 dirty.renameTo(clean); 530 long oldLength = entry.lengths[i]; 531 long newLength = clean.length(); 532 entry.lengths[i] = newLength; 533 size = size - oldLength + newLength; 534 } 535 } else { 536 deleteIfExists(dirty); 537 } 538 } 539 540 redundantOpCount++; 541 entry.currentEditor = null; 542 if (entry.readable | success) { 543 entry.readable = true; 544 journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 545 if (success) { 546 entry.sequenceNumber = nextSequenceNumber++; 547 } 548 } else { 549 lruEntries.remove(entry.key); 550 journalWriter.write(REMOVE + ' ' + entry.key + '\n'); 551 } 552 journalWriter.flush(); 553 554 if (size > maxSize || journalRebuildRequired()) { 555 executorService.submit(cleanupCallable); 556 } 557 } 558 559 /** 560 * We only rebuild the journal when it will halve the size of the journal 561 * and eliminate at least 2000 ops. 562 */ 563 private boolean journalRebuildRequired() { 564 final int redundantOpCompactThreshold = 2000; 565 return redundantOpCount >= redundantOpCompactThreshold // 566 && redundantOpCount >= lruEntries.size(); 567 } 568 569 /** 570 * Drops the entry for {@code key} if it exists and can be removed. Entries 571 * actively being edited cannot be removed. 572 * 573 * @return true if an entry was removed. 574 */ 575 public synchronized boolean remove(String key) throws IOException { 576 checkNotClosed(); 577 validateKey(key); 578 Entry entry = lruEntries.get(key); 579 if (entry == null || entry.currentEditor != null) { 580 return false; 581 } 582 583 for (int i = 0; i < valueCount; i++) { 584 File file = entry.getCleanFile(i); 585 if (!file.delete()) { 586 throw new IOException("failed to delete " + file); 587 } 588 size -= entry.lengths[i]; 589 entry.lengths[i] = 0; 590 } 591 592 redundantOpCount++; 593 journalWriter.append(REMOVE + ' ' + key + '\n'); 594 lruEntries.remove(key); 595 596 if (journalRebuildRequired()) { 597 executorService.submit(cleanupCallable); 598 } 599 600 return true; 601 } 602 603 /** Returns true if this cache has been closed. */ 604 public boolean isClosed() { 605 return journalWriter == null; 606 } 607 608 private void checkNotClosed() { 609 if (journalWriter == null) { 610 throw new IllegalStateException("cache is closed"); 611 } 612 } 613 614 /** Force buffered operations to the filesystem. */ 615 public synchronized void flush() throws IOException { 616 checkNotClosed(); 617 trimToSize(); 618 journalWriter.flush(); 619 } 620 621 /** Closes this cache. Stored values will remain on the filesystem. */ 622 public synchronized void close() throws IOException { 623 if (journalWriter == null) { 624 return; // Already closed. 625 } 626 for (Entry entry : new ArrayList<Entry>(lruEntries.values())) { 627 if (entry.currentEditor != null) { 628 entry.currentEditor.abort(); 629 } 630 } 631 trimToSize(); 632 journalWriter.close(); 633 journalWriter = null; 634 } 635 636 private void trimToSize() throws IOException { 637 while (size > maxSize) { 638 Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); 639 remove(toEvict.getKey()); 640 } 641 } 642 643 /** 644 * Closes the cache and deletes all of its stored values. This will delete 645 * all files in the cache directory including files that weren't created by 646 * the cache. 647 */ 648 public void delete() throws IOException { 649 close(); 650 Util.deleteContents(directory); 651 } 652 653 private void validateKey(String key) { 654 Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); 655 if (!matcher.matches()) { 656 throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\""); 657 } 658 } 659 660 private static String inputStreamToString(InputStream in) throws IOException { 661 return Util.readFully(new InputStreamReader(in, Util.UTF_8)); 662 } 663 664 /** A snapshot of the values for an entry. */ 665 public final class Snapshot implements Closeable { 666 private final String key; 667 private final long sequenceNumber; 668 private final InputStream[] ins; 669 private final long[] lengths; 670 671 private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { 672 this.key = key; 673 this.sequenceNumber = sequenceNumber; 674 this.ins = ins; 675 this.lengths = lengths; 676 } 677 678 /** 679 * Returns an editor for this snapshot's entry, or null if either the 680 * entry has changed since this snapshot was created or if another edit 681 * is in progress. 682 */ 683 public Editor edit() throws IOException { 684 return DiskLruCache.this.edit(key, sequenceNumber); 685 } 686 687 /** Returns the unbuffered stream with the value for {@code index}. */ 688 public InputStream getInputStream(int index) { 689 return ins[index]; 690 } 691 692 /** Returns the string value for {@code index}. */ 693 public String getString(int index) throws IOException { 694 return inputStreamToString(getInputStream(index)); 695 } 696 697 /** Returns the byte length of the value for {@code index}. */ 698 public long getLength(int index) { 699 return lengths[index]; 700 } 701 702 public void close() { 703 for (InputStream in : ins) { 704 Util.closeQuietly(in); 705 } 706 } 707 } 708 709 private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { 710 @Override 711 public void write(int b) throws IOException { 712 // Eat all writes silently. Nom nom. 713 } 714 }; 715 716 /** Edits the values for an entry. */ 717 public final class Editor { 718 private final Entry entry; 719 private final boolean[] written; 720 private boolean hasErrors; 721 private boolean committed; 722 723 private Editor(Entry entry) { 724 this.entry = entry; 725 this.written = (entry.readable) ? null : new boolean[valueCount]; 726 } 727 728 /** 729 * Returns an unbuffered input stream to read the last committed value, 730 * or null if no value has been committed. 731 */ 732 public InputStream newInputStream(int index) throws IOException { 733 synchronized (DiskLruCache.this) { 734 if (entry.currentEditor != this) { 735 throw new IllegalStateException(); 736 } 737 if (!entry.readable) { 738 return null; 739 } 740 try { 741 return new FileInputStream(entry.getCleanFile(index)); 742 } catch (FileNotFoundException e) { 743 return null; 744 } 745 } 746 } 747 748 /** 749 * Returns the last committed value as a string, or null if no value 750 * has been committed. 751 */ 752 public String getString(int index) throws IOException { 753 InputStream in = newInputStream(index); 754 return in != null ? inputStreamToString(in) : null; 755 } 756 757 /** 758 * Returns a new unbuffered output stream to write the value at 759 * {@code index}. If the underlying output stream encounters errors 760 * when writing to the filesystem, this edit will be aborted when 761 * {@link #commit} is called. The returned output stream does not throw 762 * IOExceptions. 763 */ 764 public OutputStream newOutputStream(int index) throws IOException { 765 synchronized (DiskLruCache.this) { 766 if (entry.currentEditor != this) { 767 throw new IllegalStateException(); 768 } 769 if (!entry.readable) { 770 written[index] = true; 771 } 772 File dirtyFile = entry.getDirtyFile(index); 773 FileOutputStream outputStream; 774 try { 775 outputStream = new FileOutputStream(dirtyFile); 776 } catch (FileNotFoundException e) { 777 // Attempt to recreate the cache directory. 778 directory.mkdirs(); 779 try { 780 outputStream = new FileOutputStream(dirtyFile); 781 } catch (FileNotFoundException e2) { 782 // We are unable to recover. Silently eat the writes. 783 return NULL_OUTPUT_STREAM; 784 } 785 } 786 return new FaultHidingOutputStream(outputStream); 787 } 788 } 789 790 /** Sets the value at {@code index} to {@code value}. */ 791 public void set(int index, String value) throws IOException { 792 Writer writer = null; 793 try { 794 writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8); 795 writer.write(value); 796 } finally { 797 Util.closeQuietly(writer); 798 } 799 } 800 801 /** 802 * Commits this edit so it is visible to readers. This releases the 803 * edit lock so another edit may be started on the same key. 804 */ 805 public void commit() throws IOException { 806 if (hasErrors) { 807 completeEdit(this, false); 808 remove(entry.key); // The previous entry is stale. 809 } else { 810 completeEdit(this, true); 811 } 812 committed = true; 813 } 814 815 /** 816 * Aborts this edit. This releases the edit lock so another edit may be 817 * started on the same key. 818 */ 819 public void abort() throws IOException { 820 completeEdit(this, false); 821 } 822 823 public void abortUnlessCommitted() { 824 if (!committed) { 825 try { 826 abort(); 827 } catch (IOException ignored) { 828 } 829 } 830 } 831 832 private class FaultHidingOutputStream extends FilterOutputStream { 833 private FaultHidingOutputStream(OutputStream out) { 834 super(out); 835 } 836 837 @Override public void write(int oneByte) { 838 try { 839 out.write(oneByte); 840 } catch (IOException e) { 841 hasErrors = true; 842 } 843 } 844 845 @Override public void write(byte[] buffer, int offset, int length) { 846 try { 847 out.write(buffer, offset, length); 848 } catch (IOException e) { 849 hasErrors = true; 850 } 851 } 852 853 @Override public void close() { 854 try { 855 out.close(); 856 } catch (IOException e) { 857 hasErrors = true; 858 } 859 } 860 861 @Override public void flush() { 862 try { 863 out.flush(); 864 } catch (IOException e) { 865 hasErrors = true; 866 } 867 } 868 } 869 } 870 871 private final class Entry { 872 private final String key; 873 874 /** Lengths of this entry's files. */ 875 private final long[] lengths; 876 877 /** True if this entry has ever been published. */ 878 private boolean readable; 879 880 /** The ongoing edit or null if this entry is not being edited. */ 881 private Editor currentEditor; 882 883 /** The sequence number of the most recently committed edit to this entry. */ 884 private long sequenceNumber; 885 886 private Entry(String key) { 887 this.key = key; 888 this.lengths = new long[valueCount]; 889 } 890 891 public String getLengths() throws IOException { 892 StringBuilder result = new StringBuilder(); 893 for (long size : lengths) { 894 result.append(' ').append(size); 895 } 896 return result.toString(); 897 } 898 899 /** Set lengths using decimal numbers like "10123". */ 900 private void setLengths(String[] strings) throws IOException { 901 if (strings.length != valueCount) { 902 throw invalidLengths(strings); 903 } 904 905 try { 906 for (int i = 0; i < strings.length; i++) { 907 lengths[i] = Long.parseLong(strings[i]); 908 } 909 } catch (NumberFormatException e) { 910 throw invalidLengths(strings); 911 } 912 } 913 914 private IOException invalidLengths(String[] strings) throws IOException { 915 throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); 916 } 917 918 public File getCleanFile(int i) { 919 return new File(directory, key + "." + i); 920 } 921 922 public File getDirtyFile(int i) { 923 return new File(directory, key + "." + i + ".tmp"); 924 } 925 } 926} 927