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