1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.bumptech.glide.disklrucache; 18 19 import java.io.BufferedWriter; 20 import java.io.Closeable; 21 import java.io.EOFException; 22 import java.io.File; 23 import java.io.FileInputStream; 24 import java.io.FileNotFoundException; 25 import java.io.FileOutputStream; 26 import java.io.IOException; 27 import java.io.InputStream; 28 import java.io.InputStreamReader; 29 import java.io.OutputStream; 30 import java.io.OutputStreamWriter; 31 import java.io.Writer; 32 import java.util.ArrayList; 33 import java.util.Iterator; 34 import java.util.LinkedHashMap; 35 import java.util.Map; 36 import java.util.concurrent.Callable; 37 import java.util.concurrent.LinkedBlockingQueue; 38 import java.util.concurrent.ThreadPoolExecutor; 39 import java.util.concurrent.TimeUnit; 40 41 /** 42 * A cache that uses a bounded amount of space on a filesystem. Each cache 43 * entry has a string key and a fixed number of values. Each key must match 44 * the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences, 45 * accessible as streams or files. Each value must be between {@code 0} and 46 * {@code Integer.MAX_VALUE} bytes in length. 47 * 48 * <p>The cache stores its data in a directory on the filesystem. This 49 * directory must be exclusive to the cache; the cache may delete or overwrite 50 * files from its directory. It is an error for multiple processes to use the 51 * same cache directory at the same time. 52 * 53 * <p>This cache limits the number of bytes that it will store on the 54 * filesystem. When the number of stored bytes exceeds the limit, the cache will 55 * remove entries in the background until the limit is satisfied. The limit is 56 * not strict: the cache may temporarily exceed it while waiting for files to be 57 * deleted. The limit does not include filesystem overhead or the cache 58 * journal so space-sensitive applications should set a conservative limit. 59 * 60 * <p>Clients call {@link #edit} to create or update the values of an entry. An 61 * entry may have only one editor at one time; if a value is not available to be 62 * edited then {@link #edit} will return null. 63 * <ul> 64 * <li>When an entry is being <strong>created</strong> it is necessary to 65 * supply a full set of values; the empty value should be used as a 66 * placeholder if necessary. 67 * <li>When an entry is being <strong>edited</strong>, it is not necessary 68 * to supply data for every value; values default to their previous 69 * value. 70 * </ul> 71 * Every {@link #edit} call must be matched by a call to {@link Editor#commit} 72 * or {@link Editor#abort}. Committing is atomic: a read observes the full set 73 * of values as they were before or after the commit, but never a mix of values. 74 * 75 * <p>Clients call {@link #get} to read a snapshot of an entry. The read will 76 * observe the value at the time that {@link #get} was called. Updates and 77 * removals after the call do not impact ongoing reads. 78 * 79 * <p>This class is tolerant of some I/O errors. If files are missing from the 80 * filesystem, the corresponding entries will be dropped from the cache. If 81 * an error occurs while writing a cache value, the edit will fail silently. 82 * Callers should handle other problems by catching {@code IOException} and 83 * responding appropriately. 84 */ 85 public final class DiskLruCache implements Closeable { 86 static final String JOURNAL_FILE = "journal"; 87 static final String JOURNAL_FILE_TEMP = "journal.tmp"; 88 static final String JOURNAL_FILE_BACKUP = "journal.bkp"; 89 static final String MAGIC = "libcore.io.DiskLruCache"; 90 static final String VERSION_1 = "1"; 91 static final long ANY_SEQUENCE_NUMBER = -1; 92 private static final String CLEAN = "CLEAN"; 93 private static final String DIRTY = "DIRTY"; 94 private static final String REMOVE = "REMOVE"; 95 private static final String READ = "READ"; 96 97 /* 98 * This cache uses a journal file named "journal". A typical journal file 99 * looks like this: 100 * libcore.io.DiskLruCache 101 * 1 102 * 100 103 * 2 104 * 105 * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 106 * DIRTY 335c4c6028171cfddfbaae1a9c313c52 107 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 108 * REMOVE 335c4c6028171cfddfbaae1a9c313c52 109 * DIRTY 1ab96a171faeeee38496d8b330771a7a 110 * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 111 * READ 335c4c6028171cfddfbaae1a9c313c52 112 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 113 * 114 * The first five lines of the journal form its header. They are the 115 * constant string "libcore.io.DiskLruCache", the disk cache's version, 116 * the application's version, the value count, and a blank line. 117 * 118 * Each of the subsequent lines in the file is a record of the state of a 119 * cache entry. Each line contains space-separated values: a state, a key, 120 * and optional state-specific values. 121 * o DIRTY lines track that an entry is actively being created or updated. 122 * Every successful DIRTY action should be followed by a CLEAN or REMOVE 123 * action. DIRTY lines without a matching CLEAN or REMOVE indicate that 124 * temporary files may need to be deleted. 125 * o CLEAN lines track a cache entry that has been successfully published 126 * and may be read. A publish line is followed by the lengths of each of 127 * its values. 128 * o READ lines track accesses for LRU. 129 * o REMOVE lines track entries that have been deleted. 130 * 131 * The journal file is appended to as cache operations occur. The journal may 132 * occasionally be compacted by dropping redundant lines. A temporary file named 133 * "journal.tmp" will be used during compaction; that file should be deleted if 134 * it exists when the cache is opened. 135 */ 136 137 private final File directory; 138 private final File journalFile; 139 private final File journalFileTmp; 140 private final File journalFileBackup; 141 private final int appVersion; 142 private long maxSize; 143 private final int valueCount; 144 private long size = 0; 145 private Writer journalWriter; 146 private final LinkedHashMap<String, Entry> lruEntries = 147 new LinkedHashMap<String, Entry>(0, 0.75f, true); 148 private int redundantOpCount; 149 150 /** 151 * To differentiate between old and current snapshots, each entry is given 152 * a sequence number each time an edit is committed. A snapshot is stale if 153 * its sequence number is not equal to its entry's sequence number. 154 */ 155 private long nextSequenceNumber = 0; 156 157 /** This cache uses a single background thread to evict entries. */ 158 final ThreadPoolExecutor executorService = 159 new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); 160 private final Callable<Void> cleanupCallable = new Callable<Void>() { 161 public Void call() throws Exception { 162 synchronized (DiskLruCache.this) { 163 if (journalWriter == null) { 164 return null; // Closed. 165 } 166 trimToSize(); 167 if (journalRebuildRequired()) { 168 rebuildJournal(); 169 redundantOpCount = 0; 170 } 171 } 172 return null; 173 } 174 }; 175 DiskLruCache(File directory, int appVersion, int valueCount, long maxSize)176 private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { 177 this.directory = directory; 178 this.appVersion = appVersion; 179 this.journalFile = new File(directory, JOURNAL_FILE); 180 this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); 181 this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); 182 this.valueCount = valueCount; 183 this.maxSize = maxSize; 184 } 185 186 /** 187 * Opens the cache in {@code directory}, creating a cache if none exists 188 * there. 189 * 190 * @param directory a writable directory 191 * @param valueCount the number of values per cache entry. Must be positive. 192 * @param maxSize the maximum number of bytes this cache should use to store 193 * @throws IOException if reading or writing the cache directory fails 194 */ open(File directory, int appVersion, int valueCount, long maxSize)195 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 196 throws IOException { 197 if (maxSize <= 0) { 198 throw new IllegalArgumentException("maxSize <= 0"); 199 } 200 if (valueCount <= 0) { 201 throw new IllegalArgumentException("valueCount <= 0"); 202 } 203 204 // If a bkp file exists, use it instead. 205 File backupFile = new File(directory, JOURNAL_FILE_BACKUP); 206 if (backupFile.exists()) { 207 File journalFile = new File(directory, JOURNAL_FILE); 208 // If journal file also exists just delete backup file. 209 if (journalFile.exists()) { 210 backupFile.delete(); 211 } else { 212 renameTo(backupFile, journalFile, false); 213 } 214 } 215 216 // Prefer to pick up where we left off. 217 DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 218 if (cache.journalFile.exists()) { 219 try { 220 cache.readJournal(); 221 cache.processJournal(); 222 return cache; 223 } catch (IOException journalIsCorrupt) { 224 System.out 225 .println("DiskLruCache " 226 + directory 227 + " is corrupt: " 228 + journalIsCorrupt.getMessage() 229 + ", removing"); 230 cache.delete(); 231 } 232 } 233 234 // Create a new empty cache. 235 directory.mkdirs(); 236 cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 237 cache.rebuildJournal(); 238 return cache; 239 } 240 readJournal()241 private void readJournal() throws IOException { 242 StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); 243 try { 244 String magic = reader.readLine(); 245 String version = reader.readLine(); 246 String appVersionString = reader.readLine(); 247 String valueCountString = reader.readLine(); 248 String blank = reader.readLine(); 249 if (!MAGIC.equals(magic) 250 || !VERSION_1.equals(version) 251 || !Integer.toString(appVersion).equals(appVersionString) 252 || !Integer.toString(valueCount).equals(valueCountString) 253 || !"".equals(blank)) { 254 throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " 255 + valueCountString + ", " + blank + "]"); 256 } 257 258 int lineCount = 0; 259 while (true) { 260 try { 261 readJournalLine(reader.readLine()); 262 lineCount++; 263 } catch (EOFException endOfJournal) { 264 break; 265 } 266 } 267 redundantOpCount = lineCount - lruEntries.size(); 268 269 // If we ended on a truncated line, rebuild the journal before appending to it. 270 if (reader.hasUnterminatedLine()) { 271 rebuildJournal(); 272 } else { 273 journalWriter = new BufferedWriter(new OutputStreamWriter( 274 new FileOutputStream(journalFile, true), Util.US_ASCII)); 275 } 276 } finally { 277 Util.closeQuietly(reader); 278 } 279 } 280 readJournalLine(String line)281 private void readJournalLine(String line) throws IOException { 282 int firstSpace = line.indexOf(' '); 283 if (firstSpace == -1) { 284 throw new IOException("unexpected journal line: " + line); 285 } 286 287 int keyBegin = firstSpace + 1; 288 int secondSpace = line.indexOf(' ', keyBegin); 289 final String key; 290 if (secondSpace == -1) { 291 key = line.substring(keyBegin); 292 if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { 293 lruEntries.remove(key); 294 return; 295 } 296 } else { 297 key = line.substring(keyBegin, secondSpace); 298 } 299 300 Entry entry = lruEntries.get(key); 301 if (entry == null) { 302 entry = new Entry(key); 303 lruEntries.put(key, entry); 304 } 305 306 if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { 307 String[] parts = line.substring(secondSpace + 1).split(" "); 308 entry.readable = true; 309 entry.currentEditor = null; 310 entry.setLengths(parts); 311 } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { 312 entry.currentEditor = new Editor(entry); 313 } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { 314 // This work was already done by calling lruEntries.get(). 315 } else { 316 throw new IOException("unexpected journal line: " + line); 317 } 318 } 319 320 /** 321 * Computes the initial size and collects garbage as a part of opening the 322 * cache. Dirty entries are assumed to be inconsistent and will be deleted. 323 */ processJournal()324 private void processJournal() throws IOException { 325 deleteIfExists(journalFileTmp); 326 for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) { 327 Entry entry = i.next(); 328 if (entry.currentEditor == null) { 329 for (int t = 0; t < valueCount; t++) { 330 size += entry.lengths[t]; 331 } 332 } else { 333 entry.currentEditor = null; 334 for (int t = 0; t < valueCount; t++) { 335 deleteIfExists(entry.getCleanFile(t)); 336 deleteIfExists(entry.getDirtyFile(t)); 337 } 338 i.remove(); 339 } 340 } 341 } 342 343 /** 344 * Creates a new journal that omits redundant information. This replaces the 345 * current journal if it exists. 346 */ rebuildJournal()347 private synchronized void rebuildJournal() throws IOException { 348 if (journalWriter != null) { 349 journalWriter.close(); 350 } 351 352 Writer writer = new BufferedWriter( 353 new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); 354 try { 355 writer.write(MAGIC); 356 writer.write("\n"); 357 writer.write(VERSION_1); 358 writer.write("\n"); 359 writer.write(Integer.toString(appVersion)); 360 writer.write("\n"); 361 writer.write(Integer.toString(valueCount)); 362 writer.write("\n"); 363 writer.write("\n"); 364 365 for (Entry entry : lruEntries.values()) { 366 if (entry.currentEditor != null) { 367 writer.write(DIRTY + ' ' + entry.key + '\n'); 368 } else { 369 writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 370 } 371 } 372 } finally { 373 writer.close(); 374 } 375 376 if (journalFile.exists()) { 377 renameTo(journalFile, journalFileBackup, true); 378 } 379 renameTo(journalFileTmp, journalFile, false); 380 journalFileBackup.delete(); 381 382 journalWriter = new BufferedWriter( 383 new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); 384 } 385 deleteIfExists(File file)386 private static void deleteIfExists(File file) throws IOException { 387 if (file.exists() && !file.delete()) { 388 throw new IOException(); 389 } 390 } 391 renameTo(File from, File to, boolean deleteDestination)392 private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { 393 if (deleteDestination) { 394 deleteIfExists(to); 395 } 396 if (!from.renameTo(to)) { 397 throw new IOException(); 398 } 399 } 400 401 /** 402 * Returns a snapshot of the entry named {@code key}, or null if it doesn't 403 * exist is not currently readable. If a value is returned, it is moved to 404 * the head of the LRU queue. 405 */ get(String key)406 public synchronized Value get(String key) throws IOException { 407 checkNotClosed(); 408 Entry entry = lruEntries.get(key); 409 if (entry == null) { 410 return null; 411 } 412 413 if (!entry.readable) { 414 return null; 415 } 416 417 for (File file : entry.cleanFiles) { 418 // A file must have been deleted manually! 419 if (!file.exists()) { 420 return null; 421 } 422 } 423 424 redundantOpCount++; 425 journalWriter.append(READ); 426 journalWriter.append(' '); 427 journalWriter.append(key); 428 journalWriter.append('\n'); 429 if (journalRebuildRequired()) { 430 executorService.submit(cleanupCallable); 431 } 432 433 return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths); 434 } 435 436 /** 437 * Returns an editor for the entry named {@code key}, or null if another 438 * edit is in progress. 439 */ edit(String key)440 public Editor edit(String key) throws IOException { 441 return edit(key, ANY_SEQUENCE_NUMBER); 442 } 443 edit(String key, long expectedSequenceNumber)444 private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { 445 checkNotClosed(); 446 Entry entry = lruEntries.get(key); 447 if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null 448 || entry.sequenceNumber != expectedSequenceNumber)) { 449 return null; // Value is stale. 450 } 451 if (entry == null) { 452 entry = new Entry(key); 453 lruEntries.put(key, entry); 454 } else if (entry.currentEditor != null) { 455 return null; // Another edit is in progress. 456 } 457 458 Editor editor = new Editor(entry); 459 entry.currentEditor = editor; 460 461 // Flush the journal before creating files to prevent file leaks. 462 journalWriter.append(DIRTY); 463 journalWriter.append(' '); 464 journalWriter.append(key); 465 journalWriter.append('\n'); 466 journalWriter.flush(); 467 return editor; 468 } 469 470 /** Returns the directory where this cache stores its data. */ getDirectory()471 public File getDirectory() { 472 return directory; 473 } 474 475 /** 476 * Returns the maximum number of bytes that this cache should use to store 477 * its data. 478 */ getMaxSize()479 public synchronized long getMaxSize() { 480 return maxSize; 481 } 482 483 /** 484 * Changes the maximum number of bytes the cache can store and queues a job 485 * to trim the existing store, if necessary. 486 */ setMaxSize(long maxSize)487 public synchronized void setMaxSize(long maxSize) { 488 this.maxSize = maxSize; 489 executorService.submit(cleanupCallable); 490 } 491 492 /** 493 * Returns the number of bytes currently being used to store the values in 494 * this cache. This may be greater than the max size if a background 495 * deletion is pending. 496 */ size()497 public synchronized long size() { 498 return size; 499 } 500 completeEdit(Editor editor, boolean success)501 private synchronized void completeEdit(Editor editor, boolean success) throws IOException { 502 Entry entry = editor.entry; 503 if (entry.currentEditor != editor) { 504 throw new IllegalStateException(); 505 } 506 507 // If this edit is creating the entry for the first time, every index must have a value. 508 if (success && !entry.readable) { 509 for (int i = 0; i < valueCount; i++) { 510 if (!editor.written[i]) { 511 editor.abort(); 512 throw new IllegalStateException("Newly created entry didn't create value for index " + i); 513 } 514 if (!entry.getDirtyFile(i).exists()) { 515 editor.abort(); 516 return; 517 } 518 } 519 } 520 521 for (int i = 0; i < valueCount; i++) { 522 File dirty = entry.getDirtyFile(i); 523 if (success) { 524 if (dirty.exists()) { 525 File clean = entry.getCleanFile(i); 526 dirty.renameTo(clean); 527 long oldLength = entry.lengths[i]; 528 long newLength = clean.length(); 529 entry.lengths[i] = newLength; 530 size = size - oldLength + newLength; 531 } 532 } else { 533 deleteIfExists(dirty); 534 } 535 } 536 537 redundantOpCount++; 538 entry.currentEditor = null; 539 if (entry.readable | success) { 540 entry.readable = true; 541 journalWriter.append(CLEAN); 542 journalWriter.append(' '); 543 journalWriter.append(entry.key); 544 journalWriter.append(entry.getLengths()); 545 journalWriter.append('\n'); 546 547 if (success) { 548 entry.sequenceNumber = nextSequenceNumber++; 549 } 550 } else { 551 lruEntries.remove(entry.key); 552 journalWriter.append(REMOVE); 553 journalWriter.append(' '); 554 journalWriter.append(entry.key); 555 journalWriter.append('\n'); 556 } 557 journalWriter.flush(); 558 559 if (size > maxSize || journalRebuildRequired()) { 560 executorService.submit(cleanupCallable); 561 } 562 } 563 564 /** 565 * We only rebuild the journal when it will halve the size of the journal 566 * and eliminate at least 2000 ops. 567 */ journalRebuildRequired()568 private boolean journalRebuildRequired() { 569 final int redundantOpCompactThreshold = 2000; 570 return redundantOpCount >= redundantOpCompactThreshold // 571 && redundantOpCount >= lruEntries.size(); 572 } 573 574 /** 575 * Drops the entry for {@code key} if it exists and can be removed. Entries 576 * actively being edited cannot be removed. 577 * 578 * @return true if an entry was removed. 579 */ remove(String key)580 public synchronized boolean remove(String key) throws IOException { 581 checkNotClosed(); 582 Entry entry = lruEntries.get(key); 583 if (entry == null || entry.currentEditor != null) { 584 return false; 585 } 586 587 for (int i = 0; i < valueCount; i++) { 588 File file = entry.getCleanFile(i); 589 if (file.exists() && !file.delete()) { 590 throw new IOException("failed to delete " + file); 591 } 592 size -= entry.lengths[i]; 593 entry.lengths[i] = 0; 594 } 595 596 redundantOpCount++; 597 journalWriter.append(REMOVE); 598 journalWriter.append(' '); 599 journalWriter.append(key); 600 journalWriter.append('\n'); 601 602 lruEntries.remove(key); 603 604 if (journalRebuildRequired()) { 605 executorService.submit(cleanupCallable); 606 } 607 608 return true; 609 } 610 611 /** Returns true if this cache has been closed. */ isClosed()612 public synchronized boolean isClosed() { 613 return journalWriter == null; 614 } 615 checkNotClosed()616 private void checkNotClosed() { 617 if (journalWriter == null) { 618 throw new IllegalStateException("cache is closed"); 619 } 620 } 621 622 /** Force buffered operations to the filesystem. */ flush()623 public synchronized void flush() throws IOException { 624 checkNotClosed(); 625 trimToSize(); 626 journalWriter.flush(); 627 } 628 629 /** Closes this cache. Stored values will remain on the filesystem. */ close()630 public synchronized void close() throws IOException { 631 if (journalWriter == null) { 632 return; // Already closed. 633 } 634 for (Entry entry : new ArrayList<Entry>(lruEntries.values())) { 635 if (entry.currentEditor != null) { 636 entry.currentEditor.abort(); 637 } 638 } 639 trimToSize(); 640 journalWriter.close(); 641 journalWriter = null; 642 } 643 trimToSize()644 private void trimToSize() throws IOException { 645 while (size > maxSize) { 646 Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); 647 remove(toEvict.getKey()); 648 } 649 } 650 651 /** 652 * Closes the cache and deletes all of its stored values. This will delete 653 * all files in the cache directory including files that weren't created by 654 * the cache. 655 */ delete()656 public void delete() throws IOException { 657 close(); 658 Util.deleteContents(directory); 659 } 660 inputStreamToString(InputStream in)661 private static String inputStreamToString(InputStream in) throws IOException { 662 return Util.readFully(new InputStreamReader(in, Util.UTF_8)); 663 } 664 665 /** A snapshot of the values for an entry. */ 666 public final class Value { 667 private final String key; 668 private final long sequenceNumber; 669 private final long[] lengths; 670 private final File[] files; 671 Value(String key, long sequenceNumber, File[] files, long[] lengths)672 private Value(String key, long sequenceNumber, File[] files, long[] lengths) { 673 this.key = key; 674 this.sequenceNumber = sequenceNumber; 675 this.files = files; 676 this.lengths = lengths; 677 } 678 679 /** 680 * Returns an editor for this snapshot's entry, or null if either the 681 * entry has changed since this snapshot was created or if another edit 682 * is in progress. 683 */ edit()684 public Editor edit() throws IOException { 685 return DiskLruCache.this.edit(key, sequenceNumber); 686 } 687 getFile(int index)688 public File getFile(int index) { 689 return files[index]; 690 } 691 692 /** Returns the string value for {@code index}. */ getString(int index)693 public String getString(int index) throws IOException { 694 InputStream is = new FileInputStream(files[index]); 695 return inputStreamToString(is); 696 } 697 698 /** Returns the byte length of the value for {@code index}. */ getLength(int index)699 public long getLength(int index) { 700 return lengths[index]; 701 } 702 } 703 704 /** Edits the values for an entry. */ 705 public final class Editor { 706 private final Entry entry; 707 private final boolean[] written; 708 private boolean committed; 709 Editor(Entry entry)710 private Editor(Entry entry) { 711 this.entry = entry; 712 this.written = (entry.readable) ? null : new boolean[valueCount]; 713 } 714 715 /** 716 * Returns an unbuffered input stream to read the last committed value, 717 * or null if no value has been committed. 718 */ newInputStream(int index)719 private InputStream newInputStream(int index) throws IOException { 720 synchronized (DiskLruCache.this) { 721 if (entry.currentEditor != this) { 722 throw new IllegalStateException(); 723 } 724 if (!entry.readable) { 725 return null; 726 } 727 try { 728 return new FileInputStream(entry.getCleanFile(index)); 729 } catch (FileNotFoundException e) { 730 return null; 731 } 732 } 733 } 734 735 /** 736 * Returns the last committed value as a string, or null if no value 737 * has been committed. 738 */ getString(int index)739 public String getString(int index) throws IOException { 740 InputStream in = newInputStream(index); 741 return in != null ? inputStreamToString(in) : null; 742 } 743 getFile(int index)744 public File getFile(int index) throws IOException { 745 synchronized (DiskLruCache.this) { 746 if (entry.currentEditor != this) { 747 throw new IllegalStateException(); 748 } 749 if (!entry.readable) { 750 written[index] = true; 751 } 752 File dirtyFile = entry.getDirtyFile(index); 753 if (!directory.exists()) { 754 directory.mkdirs(); 755 } 756 return dirtyFile; 757 } 758 } 759 760 /** Sets the value at {@code index} to {@code value}. */ set(int index, String value)761 public void set(int index, String value) throws IOException { 762 Writer writer = null; 763 try { 764 OutputStream os = new FileOutputStream(getFile(index)); 765 writer = new OutputStreamWriter(os, Util.UTF_8); 766 writer.write(value); 767 } finally { 768 Util.closeQuietly(writer); 769 } 770 } 771 772 /** 773 * Commits this edit so it is visible to readers. This releases the 774 * edit lock so another edit may be started on the same key. 775 */ commit()776 public void commit() throws IOException { 777 // The object using this Editor must catch and handle any errors 778 // during the write. If there is an error and they call commit 779 // anyway, we will assume whatever they managed to write was valid. 780 // Normally they should call abort. 781 completeEdit(this, true); 782 committed = true; 783 } 784 785 /** 786 * Aborts this edit. This releases the edit lock so another edit may be 787 * started on the same key. 788 */ abort()789 public void abort() throws IOException { 790 completeEdit(this, false); 791 } 792 abortUnlessCommitted()793 public void abortUnlessCommitted() { 794 if (!committed) { 795 try { 796 abort(); 797 } catch (IOException ignored) { 798 } 799 } 800 } 801 } 802 803 private final class Entry { 804 private final String key; 805 806 /** Lengths of this entry's files. */ 807 private final long[] lengths; 808 809 /** Memoized File objects for this entry to avoid char[] allocations. */ 810 File[] cleanFiles; 811 File[] dirtyFiles; 812 813 /** True if this entry has ever been published. */ 814 private boolean readable; 815 816 /** The ongoing edit or null if this entry is not being edited. */ 817 private Editor currentEditor; 818 819 /** The sequence number of the most recently committed edit to this entry. */ 820 private long sequenceNumber; 821 Entry(String key)822 private Entry(String key) { 823 this.key = key; 824 this.lengths = new long[valueCount]; 825 cleanFiles = new File[valueCount]; 826 dirtyFiles = new File[valueCount]; 827 828 // The names are repetitive so re-use the same builder to avoid allocations. 829 StringBuilder fileBuilder = new StringBuilder(key).append('.'); 830 int truncateTo = fileBuilder.length(); 831 for (int i = 0; i < valueCount; i++) { 832 fileBuilder.append(i); 833 cleanFiles[i] = new File(directory, fileBuilder.toString()); 834 fileBuilder.append(".tmp"); 835 dirtyFiles[i] = new File(directory, fileBuilder.toString()); 836 fileBuilder.setLength(truncateTo); 837 } 838 } 839 getLengths()840 public String getLengths() throws IOException { 841 StringBuilder result = new StringBuilder(); 842 for (long size : lengths) { 843 result.append(' ').append(size); 844 } 845 return result.toString(); 846 } 847 848 /** Set lengths using decimal numbers like "10123". */ setLengths(String[] strings)849 private void setLengths(String[] strings) throws IOException { 850 if (strings.length != valueCount) { 851 throw invalidLengths(strings); 852 } 853 854 try { 855 for (int i = 0; i < strings.length; i++) { 856 lengths[i] = Long.parseLong(strings[i]); 857 } 858 } catch (NumberFormatException e) { 859 throw invalidLengths(strings); 860 } 861 } 862 invalidLengths(String[] strings)863 private IOException invalidLengths(String[] strings) throws IOException { 864 throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); 865 } 866 getCleanFile(int i)867 public File getCleanFile(int i) { 868 return cleanFiles[i]; 869 } 870 getDirtyFile(int i)871 public File getDirtyFile(int i) { 872 return dirtyFiles[i]; 873 } 874 } 875 } 876