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