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.Closeable; 20import java.io.EOFException; 21import java.io.File; 22import java.io.FileInputStream; 23import java.io.FileNotFoundException; 24import java.io.FileOutputStream; 25import java.io.FilterOutputStream; 26import java.io.IOException; 27import java.io.InputStream; 28import java.io.OutputStream; 29import java.util.Iterator; 30import java.util.LinkedHashMap; 31import java.util.Map; 32import java.util.concurrent.LinkedBlockingQueue; 33import java.util.concurrent.ThreadPoolExecutor; 34import java.util.concurrent.TimeUnit; 35import java.util.regex.Matcher; 36import java.util.regex.Pattern; 37import okio.BufferedSink; 38import okio.BufferedSource; 39import okio.OkBuffer; 40import okio.Okio; 41 42/** 43 * A cache that uses a bounded amount of space on a filesystem. Each cache 44 * entry has a string key and a fixed number of values. Each key must match 45 * the regex <strong>[a-z0-9_-]{1,64}</strong>. Values are byte sequences, 46 * accessible as streams or files. Each value must be between {@code 0} and 47 * {@code Integer.MAX_VALUE} bytes in length. 48 * 49 * <p>The cache stores its data in a directory on the filesystem. This 50 * directory must be exclusive to the cache; the cache may delete or overwrite 51 * files from its directory. It is an error for multiple processes to use the 52 * same cache directory at the same time. 53 * 54 * <p>This cache limits the number of bytes that it will store on the 55 * filesystem. When the number of stored bytes exceeds the limit, the cache will 56 * remove entries in the background until the limit is satisfied. The limit is 57 * not strict: the cache may temporarily exceed it while waiting for files to be 58 * deleted. The limit does not include filesystem overhead or the cache 59 * journal so space-sensitive applications should set a conservative limit. 60 * 61 * <p>Clients call {@link #edit} to create or update the values of an entry. An 62 * entry may have only one editor at one time; if a value is not available to be 63 * edited then {@link #edit} will return null. 64 * <ul> 65 * <li>When an entry is being <strong>created</strong> it is necessary to 66 * supply a full set of values; the empty value should be used as a 67 * placeholder if necessary. 68 * <li>When an entry is being <strong>edited</strong>, it is not necessary 69 * to supply data for every value; values default to their previous 70 * value. 71 * </ul> 72 * Every {@link #edit} call must be matched by a call to {@link Editor#commit} 73 * or {@link Editor#abort}. Committing is atomic: a read observes the full set 74 * of values as they were before or after the commit, but never a mix of values. 75 * 76 * <p>Clients call {@link #get} to read a snapshot of an entry. The read will 77 * observe the value at the time that {@link #get} was called. Updates and 78 * removals after the call do not impact ongoing reads. 79 * 80 * <p>This class is tolerant of some I/O errors. If files are missing from the 81 * filesystem, the corresponding entries will be dropped from the cache. If 82 * an error occurs while writing a cache value, the edit will fail silently. 83 * Callers should handle other problems by catching {@code IOException} and 84 * responding appropriately. 85 */ 86public final class DiskLruCache implements Closeable { 87 static final String JOURNAL_FILE = "journal"; 88 static final String JOURNAL_FILE_TEMP = "journal.tmp"; 89 static final String JOURNAL_FILE_BACKUP = "journal.bkp"; 90 static final String MAGIC = "libcore.io.DiskLruCache"; 91 static final String VERSION_1 = "1"; 92 static final long ANY_SEQUENCE_NUMBER = -1; 93 static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}"); 94 private static final String CLEAN = "CLEAN"; 95 private static final String DIRTY = "DIRTY"; 96 private static final String REMOVE = "REMOVE"; 97 private static final String READ = "READ"; 98 99 /* 100 * This cache uses a journal file named "journal". A typical journal file 101 * looks like this: 102 * libcore.io.DiskLruCache 103 * 1 104 * 100 105 * 2 106 * 107 * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 108 * DIRTY 335c4c6028171cfddfbaae1a9c313c52 109 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 110 * REMOVE 335c4c6028171cfddfbaae1a9c313c52 111 * DIRTY 1ab96a171faeeee38496d8b330771a7a 112 * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 113 * READ 335c4c6028171cfddfbaae1a9c313c52 114 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 115 * 116 * The first five lines of the journal form its header. They are the 117 * constant string "libcore.io.DiskLruCache", the disk cache's version, 118 * the application's version, the value count, and a blank line. 119 * 120 * Each of the subsequent lines in the file is a record of the state of a 121 * cache entry. Each line contains space-separated values: a state, a key, 122 * and optional state-specific values. 123 * o DIRTY lines track that an entry is actively being created or updated. 124 * Every successful DIRTY action should be followed by a CLEAN or REMOVE 125 * action. DIRTY lines without a matching CLEAN or REMOVE indicate that 126 * temporary files may need to be deleted. 127 * o CLEAN lines track a cache entry that has been successfully published 128 * and may be read. A publish line is followed by the lengths of each of 129 * its values. 130 * o READ lines track accesses for LRU. 131 * o REMOVE lines track entries that have been deleted. 132 * 133 * The journal file is appended to as cache operations occur. The journal may 134 * occasionally be compacted by dropping redundant lines. A temporary file named 135 * "journal.tmp" will be used during compaction; that file should be deleted if 136 * it exists when the cache is opened. 137 */ 138 139 private final File directory; 140 private final File journalFile; 141 private final File journalFileTmp; 142 private final File journalFileBackup; 143 private final int appVersion; 144 private long maxSize; 145 private final int valueCount; 146 private long size = 0; 147 private BufferedSink journalWriter; 148 private final LinkedHashMap<String, Entry> lruEntries = 149 new LinkedHashMap<String, Entry>(0, 0.75f, true); 150 private int redundantOpCount; 151 152 /** 153 * To differentiate between old and current snapshots, each entry is given 154 * a sequence number each time an edit is committed. A snapshot is stale if 155 * its sequence number is not equal to its entry's sequence number. 156 */ 157 private long nextSequenceNumber = 0; 158 159 /** This cache uses a single background thread to evict entries. */ 160 final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, 161 new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true)); 162 private final Runnable cleanupRunnable = new Runnable() { 163 public void run() { 164 synchronized (DiskLruCache.this) { 165 if (journalWriter == null) { 166 return; // Closed. 167 } 168 try { 169 trimToSize(); 170 if (journalRebuildRequired()) { 171 rebuildJournal(); 172 redundantOpCount = 0; 173 } 174 } catch (IOException e) { 175 throw new RuntimeException(e); 176 } 177 } 178 } 179 }; 180 181 private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { 182 this.directory = directory; 183 this.appVersion = appVersion; 184 this.journalFile = new File(directory, JOURNAL_FILE); 185 this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); 186 this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); 187 this.valueCount = valueCount; 188 this.maxSize = maxSize; 189 } 190 191 /** 192 * Opens the cache in {@code directory}, creating a cache if none exists 193 * there. 194 * 195 * @param directory a writable directory 196 * @param valueCount the number of values per cache entry. Must be positive. 197 * @param maxSize the maximum number of bytes this cache should use to store 198 * @throws IOException if reading or writing the cache directory fails 199 */ 200 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 201 throws IOException { 202 if (maxSize <= 0) { 203 throw new IllegalArgumentException("maxSize <= 0"); 204 } 205 if (valueCount <= 0) { 206 throw new IllegalArgumentException("valueCount <= 0"); 207 } 208 209 // If a bkp file exists, use it instead. 210 File backupFile = new File(directory, JOURNAL_FILE_BACKUP); 211 if (backupFile.exists()) { 212 File journalFile = new File(directory, JOURNAL_FILE); 213 // If journal file also exists just delete backup file. 214 if (journalFile.exists()) { 215 backupFile.delete(); 216 } else { 217 renameTo(backupFile, journalFile, false); 218 } 219 } 220 221 // Prefer to pick up where we left off. 222 DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 223 if (cache.journalFile.exists()) { 224 try { 225 cache.readJournal(); 226 cache.processJournal(); 227 cache.journalWriter = Okio.buffer(Okio.sink(new FileOutputStream(cache.journalFile, true))); 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 BufferedSource source = Okio.buffer(Okio.source(new FileInputStream(journalFile))); 245 try { 246 String magic = source.readUtf8LineStrict(); 247 String version = source.readUtf8LineStrict(); 248 String appVersionString = source.readUtf8LineStrict(); 249 String valueCountString = source.readUtf8LineStrict(); 250 String blank = source.readUtf8LineStrict(); 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(source.readUtf8LineStrict()); 264 lineCount++; 265 } catch (EOFException endOfJournal) { 266 break; 267 } 268 } 269 redundantOpCount = lineCount - lruEntries.size(); 270 } finally { 271 Util.closeQuietly(source); 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 BufferedSink writer = Okio.buffer(Okio.sink(new FileOutputStream(journalFileTmp))); 347 try { 348 writer.writeUtf8(MAGIC); 349 writer.writeUtf8("\n"); 350 writer.writeUtf8(VERSION_1); 351 writer.writeUtf8("\n"); 352 writer.writeUtf8(Integer.toString(appVersion)); 353 writer.writeUtf8("\n"); 354 writer.writeUtf8(Integer.toString(valueCount)); 355 writer.writeUtf8("\n"); 356 writer.writeUtf8("\n"); 357 358 for (Entry entry : lruEntries.values()) { 359 if (entry.currentEditor != null) { 360 writer.writeUtf8(DIRTY + ' ' + entry.key + '\n'); 361 } else { 362 writer.writeUtf8(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 363 } 364 } 365 } finally { 366 writer.close(); 367 } 368 369 if (journalFile.exists()) { 370 renameTo(journalFile, journalFileBackup, true); 371 } 372 renameTo(journalFileTmp, journalFile, false); 373 journalFileBackup.delete(); 374 375 journalWriter = Okio.buffer(Okio.sink(new FileOutputStream(journalFile, true))); 376 } 377 378 private static void deleteIfExists(File file) throws IOException { 379 // If delete() fails, make sure it's because the file didn't exist! 380 if (!file.delete() && file.exists()) { 381 throw new IOException("failed to delete " + file); 382 } 383 } 384 385 private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { 386 if (deleteDestination) { 387 deleteIfExists(to); 388 } 389 if (!from.renameTo(to)) { 390 throw new IOException(); 391 } 392 } 393 394 /** 395 * Returns a snapshot of the entry named {@code key}, or null if it doesn't 396 * exist is not currently readable. If a value is returned, it is moved to 397 * the head of the LRU queue. 398 */ 399 public synchronized Snapshot get(String key) throws IOException { 400 checkNotClosed(); 401 validateKey(key); 402 Entry entry = lruEntries.get(key); 403 if (entry == null) { 404 return null; 405 } 406 407 if (!entry.readable) { 408 return null; 409 } 410 411 // Open all streams eagerly to guarantee that we see a single published 412 // snapshot. If we opened streams lazily then the streams could come 413 // from different edits. 414 InputStream[] ins = new InputStream[valueCount]; 415 try { 416 for (int i = 0; i < valueCount; i++) { 417 ins[i] = new FileInputStream(entry.getCleanFile(i)); 418 } 419 } catch (FileNotFoundException e) { 420 // A file must have been deleted manually! 421 for (int i = 0; i < valueCount; i++) { 422 if (ins[i] != null) { 423 Util.closeQuietly(ins[i]); 424 } else { 425 break; 426 } 427 } 428 return null; 429 } 430 431 redundantOpCount++; 432 journalWriter.writeUtf8(READ + ' ' + key + '\n'); 433 if (journalRebuildRequired()) { 434 executorService.execute(cleanupRunnable); 435 } 436 437 return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); 438 } 439 440 /** 441 * Returns an editor for the entry named {@code key}, or null if another 442 * edit is in progress. 443 */ 444 public Editor edit(String key) throws IOException { 445 return edit(key, ANY_SEQUENCE_NUMBER); 446 } 447 448 private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { 449 checkNotClosed(); 450 validateKey(key); 451 Entry entry = lruEntries.get(key); 452 if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null 453 || entry.sequenceNumber != expectedSequenceNumber)) { 454 return null; // Snapshot is stale. 455 } 456 if (entry == null) { 457 entry = new Entry(key); 458 lruEntries.put(key, entry); 459 } else if (entry.currentEditor != null) { 460 return null; // Another edit is in progress. 461 } 462 463 Editor editor = new Editor(entry); 464 entry.currentEditor = editor; 465 466 // Flush the journal before creating files to prevent file leaks. 467 journalWriter.writeUtf8(DIRTY + ' ' + key + '\n'); 468 journalWriter.flush(); 469 return editor; 470 } 471 472 /** Returns the directory where this cache stores its data. */ 473 public File getDirectory() { 474 return directory; 475 } 476 477 /** 478 * Returns the maximum number of bytes that this cache should use to store 479 * its data. 480 */ 481 public synchronized long getMaxSize() { 482 return maxSize; 483 } 484 485 /** 486 * Changes the maximum number of bytes the cache can store and queues a job 487 * to trim the existing store, if necessary. 488 */ 489 public synchronized void setMaxSize(long maxSize) { 490 this.maxSize = maxSize; 491 executorService.execute(cleanupRunnable); 492 } 493 494 /** 495 * Returns the number of bytes currently being used to store the values in 496 * this cache. This may be greater than the max size if a background 497 * deletion is pending. 498 */ 499 public synchronized long size() { 500 return size; 501 } 502 503 private synchronized void completeEdit(Editor editor, boolean success) throws IOException { 504 Entry entry = editor.entry; 505 if (entry.currentEditor != editor) { 506 throw new IllegalStateException(); 507 } 508 509 // If this edit is creating the entry for the first time, every index must have a value. 510 if (success && !entry.readable) { 511 for (int i = 0; i < valueCount; i++) { 512 if (!editor.written[i]) { 513 editor.abort(); 514 throw new IllegalStateException("Newly created entry didn't create value for index " + i); 515 } 516 if (!entry.getDirtyFile(i).exists()) { 517 editor.abort(); 518 return; 519 } 520 } 521 } 522 523 for (int i = 0; i < valueCount; i++) { 524 File dirty = entry.getDirtyFile(i); 525 if (success) { 526 if (dirty.exists()) { 527 File clean = entry.getCleanFile(i); 528 dirty.renameTo(clean); 529 long oldLength = entry.lengths[i]; 530 long newLength = clean.length(); 531 entry.lengths[i] = newLength; 532 size = size - oldLength + newLength; 533 } 534 } else { 535 deleteIfExists(dirty); 536 } 537 } 538 539 redundantOpCount++; 540 entry.currentEditor = null; 541 if (entry.readable | success) { 542 entry.readable = true; 543 journalWriter.writeUtf8(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 544 if (success) { 545 entry.sequenceNumber = nextSequenceNumber++; 546 } 547 } else { 548 lruEntries.remove(entry.key); 549 journalWriter.writeUtf8(REMOVE + ' ' + entry.key + '\n'); 550 } 551 journalWriter.flush(); 552 553 if (size > maxSize || journalRebuildRequired()) { 554 executorService.execute(cleanupRunnable); 555 } 556 } 557 558 /** 559 * We only rebuild the journal when it will halve the size of the journal 560 * and eliminate at least 2000 ops. 561 */ 562 private boolean journalRebuildRequired() { 563 final int redundantOpCompactThreshold = 2000; 564 return redundantOpCount >= redundantOpCompactThreshold 565 && redundantOpCount >= lruEntries.size(); 566 } 567 568 /** 569 * Drops the entry for {@code key} if it exists and can be removed. Entries 570 * actively being edited cannot be removed. 571 * 572 * @return true if an entry was removed. 573 */ 574 public synchronized boolean remove(String key) throws IOException { 575 checkNotClosed(); 576 validateKey(key); 577 Entry entry = lruEntries.get(key); 578 if (entry == null || entry.currentEditor != null) { 579 return false; 580 } 581 582 for (int i = 0; i < valueCount; i++) { 583 File file = entry.getCleanFile(i); 584 deleteIfExists(file); 585 size -= entry.lengths[i]; 586 entry.lengths[i] = 0; 587 } 588 589 redundantOpCount++; 590 journalWriter.writeUtf8(REMOVE + ' ' + key + '\n'); 591 lruEntries.remove(key); 592 593 if (journalRebuildRequired()) { 594 executorService.execute(cleanupRunnable); 595 } 596 597 return true; 598 } 599 600 /** Returns true if this cache has been closed. */ 601 public boolean isClosed() { 602 return journalWriter == null; 603 } 604 605 private void checkNotClosed() { 606 if (journalWriter == null) { 607 throw new IllegalStateException("cache is closed"); 608 } 609 } 610 611 /** Force buffered operations to the filesystem. */ 612 public synchronized void flush() throws IOException { 613 checkNotClosed(); 614 trimToSize(); 615 journalWriter.flush(); 616 } 617 618 /** Closes this cache. Stored values will remain on the filesystem. */ 619 public synchronized void close() throws IOException { 620 if (journalWriter == null) { 621 return; // Already closed. 622 } 623 // Copying for safe iteration. 624 for (Object next : lruEntries.values().toArray()) { 625 Entry entry = (Entry) next; 626 if (entry.currentEditor != null) { 627 entry.currentEditor.abort(); 628 } 629 } 630 trimToSize(); 631 journalWriter.close(); 632 journalWriter = null; 633 } 634 635 private void trimToSize() throws IOException { 636 while (size > maxSize) { 637 Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); 638 remove(toEvict.getKey()); 639 } 640 } 641 642 /** 643 * Closes the cache and deletes all of its stored values. This will delete 644 * all files in the cache directory including files that weren't created by 645 * the cache. 646 */ 647 public void delete() throws IOException { 648 close(); 649 Util.deleteContents(directory); 650 } 651 652 private void validateKey(String key) { 653 Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); 654 if (!matcher.matches()) { 655 throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\""); 656 } 657 } 658 659 private static String inputStreamToString(InputStream in) throws IOException { 660 OkBuffer buffer = Util.readFully(Okio.source(in)); 661 return buffer.readUtf8(buffer.size()); 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 BufferedSink writer = Okio.buffer(Okio.sink(newOutputStream(index))); 793 writer.writeUtf8(value); 794 writer.close(); 795 } 796 797 /** 798 * Commits this edit so it is visible to readers. This releases the 799 * edit lock so another edit may be started on the same key. 800 */ 801 public void commit() throws IOException { 802 if (hasErrors) { 803 completeEdit(this, false); 804 remove(entry.key); // The previous entry is stale. 805 } else { 806 completeEdit(this, true); 807 } 808 committed = true; 809 } 810 811 /** 812 * Aborts this edit. This releases the edit lock so another edit may be 813 * started on the same key. 814 */ 815 public void abort() throws IOException { 816 completeEdit(this, false); 817 } 818 819 public void abortUnlessCommitted() { 820 if (!committed) { 821 try { 822 abort(); 823 } catch (IOException ignored) { 824 } 825 } 826 } 827 828 private class FaultHidingOutputStream extends FilterOutputStream { 829 private FaultHidingOutputStream(OutputStream out) { 830 super(out); 831 } 832 833 @Override public void write(int oneByte) { 834 try { 835 out.write(oneByte); 836 } catch (IOException e) { 837 hasErrors = true; 838 } 839 } 840 841 @Override public void write(byte[] buffer, int offset, int length) { 842 try { 843 out.write(buffer, offset, length); 844 } catch (IOException e) { 845 hasErrors = true; 846 } 847 } 848 849 @Override public void close() { 850 try { 851 out.close(); 852 } catch (IOException e) { 853 hasErrors = true; 854 } 855 } 856 857 @Override public void flush() { 858 try { 859 out.flush(); 860 } catch (IOException e) { 861 hasErrors = true; 862 } 863 } 864 } 865 } 866 867 private final class Entry { 868 private final String key; 869 870 /** Lengths of this entry's files. */ 871 private final long[] lengths; 872 873 /** True if this entry has ever been published. */ 874 private boolean readable; 875 876 /** The ongoing edit or null if this entry is not being edited. */ 877 private Editor currentEditor; 878 879 /** The sequence number of the most recently committed edit to this entry. */ 880 private long sequenceNumber; 881 882 private Entry(String key) { 883 this.key = key; 884 this.lengths = new long[valueCount]; 885 } 886 887 public String getLengths() throws IOException { 888 StringBuilder result = new StringBuilder(); 889 for (long size : lengths) { 890 result.append(' ').append(size); 891 } 892 return result.toString(); 893 } 894 895 /** Set lengths using decimal numbers like "10123". */ 896 private void setLengths(String[] strings) throws IOException { 897 if (strings.length != valueCount) { 898 throw invalidLengths(strings); 899 } 900 901 try { 902 for (int i = 0; i < strings.length; i++) { 903 lengths[i] = Long.parseLong(strings[i]); 904 } 905 } catch (NumberFormatException e) { 906 throw invalidLengths(strings); 907 } 908 } 909 910 private IOException invalidLengths(String[] strings) throws IOException { 911 throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); 912 } 913 914 public File getCleanFile(int i) { 915 return new File(directory, key + "." + i); 916 } 917 918 public File getDirtyFile(int i) { 919 return new File(directory, key + "." + i + ".tmp"); 920 } 921 } 922} 923