| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.squareup.okhttp.internal; |
| |
| import static com.squareup.okhttp.internal.Util.UTF_8; |
| import java.io.BufferedWriter; |
| import java.io.Closeable; |
| import java.io.EOFException; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.FileWriter; |
| import java.io.FilterOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.Writer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.ThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * A cache that uses a bounded amount of space on a filesystem. Each cache |
| * entry has a string key and a fixed number of values. Values are byte |
| * sequences, accessible as streams or files. Each value must be between {@code |
| * 0} and {@code Integer.MAX_VALUE} bytes in length. |
| * |
| * <p>The cache stores its data in a directory on the filesystem. This |
| * directory must be exclusive to the cache; the cache may delete or overwrite |
| * files from its directory. It is an error for multiple processes to use the |
| * same cache directory at the same time. |
| * |
| * <p>This cache limits the number of bytes that it will store on the |
| * filesystem. When the number of stored bytes exceeds the limit, the cache will |
| * remove entries in the background until the limit is satisfied. The limit is |
| * not strict: the cache may temporarily exceed it while waiting for files to be |
| * deleted. The limit does not include filesystem overhead or the cache |
| * journal so space-sensitive applications should set a conservative limit. |
| * |
| * <p>Clients call {@link #edit} to create or update the values of an entry. An |
| * entry may have only one editor at one time; if a value is not available to be |
| * edited then {@link #edit} will return null. |
| * <ul> |
| * <li>When an entry is being <strong>created</strong> it is necessary to |
| * supply a full set of values; the empty value should be used as a |
| * placeholder if necessary. |
| * <li>When an entry is being <strong>edited</strong>, it is not necessary |
| * to supply data for every value; values default to their previous |
| * value. |
| * </ul> |
| * Every {@link #edit} call must be matched by a call to {@link Editor#commit} |
| * or {@link Editor#abort}. Committing is atomic: a read observes the full set |
| * of values as they were before or after the commit, but never a mix of values. |
| * |
| * <p>Clients call {@link #get} to read a snapshot of an entry. The read will |
| * observe the value at the time that {@link #get} was called. Updates and |
| * removals after the call do not impact ongoing reads. |
| * |
| * <p>This class is tolerant of some I/O errors. If files are missing from the |
| * filesystem, the corresponding entries will be dropped from the cache. If |
| * an error occurs while writing a cache value, the edit will fail silently. |
| * Callers should handle other problems by catching {@code IOException} and |
| * responding appropriately. |
| */ |
| public final class DiskLruCache implements Closeable { |
| static final String JOURNAL_FILE = "journal"; |
| static final String JOURNAL_FILE_TMP = "journal.tmp"; |
| static final String MAGIC = "libcore.io.DiskLruCache"; |
| static final String VERSION_1 = "1"; |
| static final long ANY_SEQUENCE_NUMBER = -1; |
| private static final String CLEAN = "CLEAN"; |
| private static final String DIRTY = "DIRTY"; |
| private static final String REMOVE = "REMOVE"; |
| private static final String READ = "READ"; |
| |
| /* |
| * This cache uses a journal file named "journal". A typical journal file |
| * looks like this: |
| * libcore.io.DiskLruCache |
| * 1 |
| * 100 |
| * 2 |
| * |
| * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 |
| * DIRTY 335c4c6028171cfddfbaae1a9c313c52 |
| * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 |
| * REMOVE 335c4c6028171cfddfbaae1a9c313c52 |
| * DIRTY 1ab96a171faeeee38496d8b330771a7a |
| * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 |
| * READ 335c4c6028171cfddfbaae1a9c313c52 |
| * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 |
| * |
| * The first five lines of the journal form its header. They are the |
| * constant string "libcore.io.DiskLruCache", the disk cache's version, |
| * the application's version, the value count, and a blank line. |
| * |
| * Each of the subsequent lines in the file is a record of the state of a |
| * cache entry. Each line contains space-separated values: a state, a key, |
| * and optional state-specific values. |
| * o DIRTY lines track that an entry is actively being created or updated. |
| * Every successful DIRTY action should be followed by a CLEAN or REMOVE |
| * action. DIRTY lines without a matching CLEAN or REMOVE indicate that |
| * temporary files may need to be deleted. |
| * o CLEAN lines track a cache entry that has been successfully published |
| * and may be read. A publish line is followed by the lengths of each of |
| * its values. |
| * o READ lines track accesses for LRU. |
| * o REMOVE lines track entries that have been deleted. |
| * |
| * The journal file is appended to as cache operations occur. The journal may |
| * occasionally be compacted by dropping redundant lines. A temporary file named |
| * "journal.tmp" will be used during compaction; that file should be deleted if |
| * it exists when the cache is opened. |
| */ |
| |
| private final File directory; |
| private final File journalFile; |
| private final File journalFileTmp; |
| private final int appVersion; |
| private final long maxSize; |
| private final int valueCount; |
| private long size = 0; |
| private Writer journalWriter; |
| private final LinkedHashMap<String, Entry> lruEntries |
| = new LinkedHashMap<String, Entry>(0, 0.75f, true); |
| private int redundantOpCount; |
| |
| /** |
| * To differentiate between old and current snapshots, each entry is given |
| * a sequence number each time an edit is committed. A snapshot is stale if |
| * its sequence number is not equal to its entry's sequence number. |
| */ |
| private long nextSequenceNumber = 0; |
| |
| /** This cache uses a single background thread to evict entries. */ |
| private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, |
| 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); |
| private final Callable<Void> cleanupCallable = new Callable<Void>() { |
| @Override public Void call() throws Exception { |
| synchronized (DiskLruCache.this) { |
| if (journalWriter == null) { |
| return null; // closed |
| } |
| trimToSize(); |
| if (journalRebuildRequired()) { |
| rebuildJournal(); |
| redundantOpCount = 0; |
| } |
| } |
| return null; |
| } |
| }; |
| |
| private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { |
| this.directory = directory; |
| this.appVersion = appVersion; |
| this.journalFile = new File(directory, JOURNAL_FILE); |
| this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); |
| this.valueCount = valueCount; |
| this.maxSize = maxSize; |
| } |
| |
| /** |
| * Opens the cache in {@code directory}, creating a cache if none exists |
| * there. |
| * |
| * @param directory a writable directory |
| * @param appVersion |
| * @param valueCount the number of values per cache entry. Must be positive. |
| * @param maxSize the maximum number of bytes this cache should use to store |
| * @throws IOException if reading or writing the cache directory fails |
| */ |
| public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) |
| throws IOException { |
| if (maxSize <= 0) { |
| throw new IllegalArgumentException("maxSize <= 0"); |
| } |
| if (valueCount <= 0) { |
| throw new IllegalArgumentException("valueCount <= 0"); |
| } |
| |
| // prefer to pick up where we left off |
| DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); |
| if (cache.journalFile.exists()) { |
| try { |
| cache.readJournal(); |
| cache.processJournal(); |
| cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true)); |
| return cache; |
| } catch (IOException journalIsCorrupt) { |
| Platform.get().logW("DiskLruCache " + directory + " is corrupt: " |
| + journalIsCorrupt.getMessage() + ", removing"); |
| cache.delete(); |
| } |
| } |
| |
| // create a new empty cache |
| directory.mkdirs(); |
| cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); |
| cache.rebuildJournal(); |
| return cache; |
| } |
| |
| private void readJournal() throws IOException { |
| StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), |
| Util.US_ASCII); |
| try { |
| String magic = reader.readLine(); |
| String version = reader.readLine(); |
| String appVersionString = reader.readLine(); |
| String valueCountString = reader.readLine(); |
| String blank = reader.readLine(); |
| if (!MAGIC.equals(magic) |
| || !VERSION_1.equals(version) |
| || !Integer.toString(appVersion).equals(appVersionString) |
| || !Integer.toString(valueCount).equals(valueCountString) |
| || !"".equals(blank)) { |
| throw new IOException("unexpected journal header: [" |
| + magic + ", " + version + ", " + valueCountString + ", " + blank + "]"); |
| } |
| |
| while (true) { |
| try { |
| readJournalLine(reader.readLine()); |
| } catch (EOFException endOfJournal) { |
| break; |
| } |
| } |
| } finally { |
| Util.closeQuietly(reader); |
| } |
| } |
| |
| private void readJournalLine(String line) throws IOException { |
| String[] parts = line.split(" "); |
| if (parts.length < 2) { |
| throw new IOException("unexpected journal line: " + line); |
| } |
| |
| String key = parts[1]; |
| if (parts[0].equals(REMOVE) && parts.length == 2) { |
| lruEntries.remove(key); |
| return; |
| } |
| |
| Entry entry = lruEntries.get(key); |
| if (entry == null) { |
| entry = new Entry(key); |
| lruEntries.put(key, entry); |
| } |
| |
| if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { |
| entry.readable = true; |
| entry.currentEditor = null; |
| entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length)); |
| } else if (parts[0].equals(DIRTY) && parts.length == 2) { |
| entry.currentEditor = new Editor(entry); |
| } else if (parts[0].equals(READ) && parts.length == 2) { |
| // this work was already done by calling lruEntries.get() |
| } else { |
| throw new IOException("unexpected journal line: " + line); |
| } |
| } |
| |
| /** |
| * Computes the initial size and collects garbage as a part of opening the |
| * cache. Dirty entries are assumed to be inconsistent and will be deleted. |
| */ |
| private void processJournal() throws IOException { |
| deleteIfExists(journalFileTmp); |
| for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) { |
| Entry entry = i.next(); |
| if (entry.currentEditor == null) { |
| for (int t = 0; t < valueCount; t++) { |
| size += entry.lengths[t]; |
| } |
| } else { |
| entry.currentEditor = null; |
| for (int t = 0; t < valueCount; t++) { |
| deleteIfExists(entry.getCleanFile(t)); |
| deleteIfExists(entry.getDirtyFile(t)); |
| } |
| i.remove(); |
| } |
| } |
| } |
| |
| /** |
| * Creates a new journal that omits redundant information. This replaces the |
| * current journal if it exists. |
| */ |
| private synchronized void rebuildJournal() throws IOException { |
| if (journalWriter != null) { |
| journalWriter.close(); |
| } |
| |
| Writer writer = new BufferedWriter(new FileWriter(journalFileTmp)); |
| writer.write(MAGIC); |
| writer.write("\n"); |
| writer.write(VERSION_1); |
| writer.write("\n"); |
| writer.write(Integer.toString(appVersion)); |
| writer.write("\n"); |
| writer.write(Integer.toString(valueCount)); |
| writer.write("\n"); |
| writer.write("\n"); |
| |
| for (Entry entry : lruEntries.values()) { |
| if (entry.currentEditor != null) { |
| writer.write(DIRTY + ' ' + entry.key + '\n'); |
| } else { |
| writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); |
| } |
| } |
| |
| writer.close(); |
| journalFileTmp.renameTo(journalFile); |
| journalWriter = new BufferedWriter(new FileWriter(journalFile, true)); |
| } |
| |
| private static void deleteIfExists(File file) throws IOException { |
| file.delete(); |
| } |
| |
| /** |
| * Returns a snapshot of the entry named {@code key}, or null if it doesn't |
| * exist is not currently readable. If a value is returned, it is moved to |
| * the head of the LRU queue. |
| */ |
| public synchronized Snapshot get(String key) throws IOException { |
| checkNotClosed(); |
| validateKey(key); |
| Entry entry = lruEntries.get(key); |
| if (entry == null) { |
| return null; |
| } |
| |
| if (!entry.readable) { |
| return null; |
| } |
| |
| /* |
| * Open all streams eagerly to guarantee that we see a single published |
| * snapshot. If we opened streams lazily then the streams could come |
| * from different edits. |
| */ |
| InputStream[] ins = new InputStream[valueCount]; |
| try { |
| for (int i = 0; i < valueCount; i++) { |
| ins[i] = new FileInputStream(entry.getCleanFile(i)); |
| } |
| } catch (FileNotFoundException e) { |
| // a file must have been deleted manually! |
| return null; |
| } |
| |
| redundantOpCount++; |
| journalWriter.append(READ + ' ' + key + '\n'); |
| if (journalRebuildRequired()) { |
| executorService.submit(cleanupCallable); |
| } |
| |
| return new Snapshot(key, entry.sequenceNumber, ins); |
| } |
| |
| /** |
| * Returns an editor for the entry named {@code key}, or null if another |
| * edit is in progress. |
| */ |
| public Editor edit(String key) throws IOException { |
| return edit(key, ANY_SEQUENCE_NUMBER); |
| } |
| |
| private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { |
| checkNotClosed(); |
| validateKey(key); |
| Entry entry = lruEntries.get(key); |
| if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER |
| && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { |
| return null; // snapshot is stale |
| } |
| if (entry == null) { |
| entry = new Entry(key); |
| lruEntries.put(key, entry); |
| } else if (entry.currentEditor != null) { |
| return null; // another edit is in progress |
| } |
| |
| Editor editor = new Editor(entry); |
| entry.currentEditor = editor; |
| |
| // flush the journal before creating files to prevent file leaks |
| journalWriter.write(DIRTY + ' ' + key + '\n'); |
| journalWriter.flush(); |
| return editor; |
| } |
| |
| /** |
| * Returns the directory where this cache stores its data. |
| */ |
| public File getDirectory() { |
| return directory; |
| } |
| |
| /** |
| * Returns the maximum number of bytes that this cache should use to store |
| * its data. |
| */ |
| public long maxSize() { |
| return maxSize; |
| } |
| |
| /** |
| * Returns the number of bytes currently being used to store the values in |
| * this cache. This may be greater than the max size if a background |
| * deletion is pending. |
| */ |
| public synchronized long size() { |
| return size; |
| } |
| |
| private synchronized void completeEdit(Editor editor, boolean success) throws IOException { |
| Entry entry = editor.entry; |
| if (entry.currentEditor != editor) { |
| throw new IllegalStateException(); |
| } |
| |
| // if this edit is creating the entry for the first time, every index must have a value |
| if (success && !entry.readable) { |
| for (int i = 0; i < valueCount; i++) { |
| if (!editor.written[i]) { |
| editor.abort(); |
| throw new IllegalStateException( |
| "Newly created entry didn't create value for index " + i); |
| } |
| if (!entry.getDirtyFile(i).exists()) { |
| editor.abort(); |
| Platform.get().logW( |
| "DiskLruCache: Newly created entry doesn't have file for index " + i); |
| return; |
| } |
| } |
| } |
| |
| for (int i = 0; i < valueCount; i++) { |
| File dirty = entry.getDirtyFile(i); |
| if (success) { |
| if (dirty.exists()) { |
| File clean = entry.getCleanFile(i); |
| dirty.renameTo(clean); |
| long oldLength = entry.lengths[i]; |
| long newLength = clean.length(); |
| entry.lengths[i] = newLength; |
| size = size - oldLength + newLength; |
| } |
| } else { |
| deleteIfExists(dirty); |
| } |
| } |
| |
| redundantOpCount++; |
| entry.currentEditor = null; |
| if (entry.readable | success) { |
| entry.readable = true; |
| journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); |
| if (success) { |
| entry.sequenceNumber = nextSequenceNumber++; |
| } |
| } else { |
| lruEntries.remove(entry.key); |
| journalWriter.write(REMOVE + ' ' + entry.key + '\n'); |
| } |
| |
| if (size > maxSize || journalRebuildRequired()) { |
| executorService.submit(cleanupCallable); |
| } |
| } |
| |
| /** |
| * We only rebuild the journal when it will halve the size of the journal |
| * and eliminate at least 2000 ops. |
| */ |
| private boolean journalRebuildRequired() { |
| final int redundantOpCompactThreshold = 2000; |
| return redundantOpCount >= redundantOpCompactThreshold |
| && redundantOpCount >= lruEntries.size(); |
| } |
| |
| /** |
| * Drops the entry for {@code key} if it exists and can be removed. Entries |
| * actively being edited cannot be removed. |
| * |
| * @return true if an entry was removed. |
| */ |
| public synchronized boolean remove(String key) throws IOException { |
| checkNotClosed(); |
| validateKey(key); |
| Entry entry = lruEntries.get(key); |
| if (entry == null || entry.currentEditor != null) { |
| return false; |
| } |
| |
| for (int i = 0; i < valueCount; i++) { |
| File file = entry.getCleanFile(i); |
| if (!file.delete()) { |
| throw new IOException("failed to delete " + file); |
| } |
| size -= entry.lengths[i]; |
| entry.lengths[i] = 0; |
| } |
| |
| redundantOpCount++; |
| journalWriter.append(REMOVE + ' ' + key + '\n'); |
| lruEntries.remove(key); |
| |
| if (journalRebuildRequired()) { |
| executorService.submit(cleanupCallable); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Returns true if this cache has been closed. |
| */ |
| public boolean isClosed() { |
| return journalWriter == null; |
| } |
| |
| private void checkNotClosed() { |
| if (journalWriter == null) { |
| throw new IllegalStateException("cache is closed"); |
| } |
| } |
| |
| /** |
| * Force buffered operations to the filesystem. |
| */ |
| public synchronized void flush() throws IOException { |
| checkNotClosed(); |
| trimToSize(); |
| journalWriter.flush(); |
| } |
| |
| /** |
| * Closes this cache. Stored values will remain on the filesystem. |
| */ |
| public synchronized void close() throws IOException { |
| if (journalWriter == null) { |
| return; // already closed |
| } |
| for (Entry entry : new ArrayList<Entry>(lruEntries.values())) { |
| if (entry.currentEditor != null) { |
| entry.currentEditor.abort(); |
| } |
| } |
| trimToSize(); |
| journalWriter.close(); |
| journalWriter = null; |
| } |
| |
| private void trimToSize() throws IOException { |
| while (size > maxSize) { |
| Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); |
| remove(toEvict.getKey()); |
| } |
| } |
| |
| /** |
| * Closes the cache and deletes all of its stored values. This will delete |
| * all files in the cache directory including files that weren't created by |
| * the cache. |
| */ |
| public void delete() throws IOException { |
| close(); |
| Util.deleteContents(directory); |
| } |
| |
| private void validateKey(String key) { |
| if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { |
| throw new IllegalArgumentException( |
| "keys must not contain spaces or newlines: \"" + key + "\""); |
| } |
| } |
| |
| private static String inputStreamToString(InputStream in) throws IOException { |
| return Util.readFully(new InputStreamReader(in, UTF_8)); |
| } |
| |
| /** |
| * A snapshot of the values for an entry. |
| */ |
| public final class Snapshot implements Closeable { |
| private final String key; |
| private final long sequenceNumber; |
| private final InputStream[] ins; |
| |
| private Snapshot(String key, long sequenceNumber, InputStream[] ins) { |
| this.key = key; |
| this.sequenceNumber = sequenceNumber; |
| this.ins = ins; |
| } |
| |
| /** |
| * Returns an editor for this snapshot's entry, or null if either the |
| * entry has changed since this snapshot was created or if another edit |
| * is in progress. |
| */ |
| public Editor edit() throws IOException { |
| return DiskLruCache.this.edit(key, sequenceNumber); |
| } |
| |
| /** |
| * Returns the unbuffered stream with the value for {@code index}. |
| */ |
| public InputStream getInputStream(int index) { |
| return ins[index]; |
| } |
| |
| /** |
| * Returns the string value for {@code index}. |
| */ |
| public String getString(int index) throws IOException { |
| return inputStreamToString(getInputStream(index)); |
| } |
| |
| @Override public void close() { |
| for (InputStream in : ins) { |
| Util.closeQuietly(in); |
| } |
| } |
| } |
| |
| /** |
| * Edits the values for an entry. |
| */ |
| public final class Editor { |
| private final Entry entry; |
| private final boolean[] written; |
| private boolean hasErrors; |
| |
| private Editor(Entry entry) { |
| this.entry = entry; |
| this.written = (entry.readable) ? null : new boolean[valueCount]; |
| } |
| |
| /** |
| * Returns an unbuffered input stream to read the last committed value, |
| * or null if no value has been committed. |
| */ |
| public InputStream newInputStream(int index) throws IOException { |
| synchronized (DiskLruCache.this) { |
| if (entry.currentEditor != this) { |
| throw new IllegalStateException(); |
| } |
| if (!entry.readable) { |
| return null; |
| } |
| return new FileInputStream(entry.getCleanFile(index)); |
| } |
| } |
| |
| /** |
| * Returns the last committed value as a string, or null if no value |
| * has been committed. |
| */ |
| public String getString(int index) throws IOException { |
| InputStream in = newInputStream(index); |
| return in != null ? inputStreamToString(in) : null; |
| } |
| |
| /** |
| * Returns a new unbuffered output stream to write the value at |
| * {@code index}. If the underlying output stream encounters errors |
| * when writing to the filesystem, this edit will be aborted when |
| * {@link #commit} is called. The returned output stream does not throw |
| * IOExceptions. |
| */ |
| public OutputStream newOutputStream(int index) throws IOException { |
| synchronized (DiskLruCache.this) { |
| if (entry.currentEditor != this) { |
| throw new IllegalStateException(); |
| } |
| if (!entry.readable) { |
| written[index] = true; |
| } |
| return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); |
| } |
| } |
| |
| /** |
| * Sets the value at {@code index} to {@code value}. |
| */ |
| public void set(int index, String value) throws IOException { |
| Writer writer = null; |
| try { |
| writer = new OutputStreamWriter(newOutputStream(index), UTF_8); |
| writer.write(value); |
| } finally { |
| Util.closeQuietly(writer); |
| } |
| } |
| |
| /** |
| * Commits this edit so it is visible to readers. This releases the |
| * edit lock so another edit may be started on the same key. |
| */ |
| public void commit() throws IOException { |
| if (hasErrors) { |
| completeEdit(this, false); |
| remove(entry.key); // the previous entry is stale |
| } else { |
| completeEdit(this, true); |
| } |
| } |
| |
| /** |
| * Aborts this edit. This releases the edit lock so another edit may be |
| * started on the same key. |
| */ |
| public void abort() throws IOException { |
| completeEdit(this, false); |
| } |
| |
| private final class FaultHidingOutputStream extends FilterOutputStream { |
| private FaultHidingOutputStream(OutputStream out) { |
| super(out); |
| } |
| |
| @Override public void write(int oneByte) { |
| try { |
| out.write(oneByte); |
| } catch (IOException e) { |
| hasErrors = true; |
| } |
| } |
| |
| @Override public void write(byte[] buffer, int offset, int length) { |
| try { |
| out.write(buffer, offset, length); |
| } catch (IOException e) { |
| hasErrors = true; |
| } |
| } |
| |
| @Override public void close() { |
| try { |
| out.close(); |
| } catch (IOException e) { |
| hasErrors = true; |
| } |
| } |
| |
| @Override public void flush() { |
| try { |
| out.flush(); |
| } catch (IOException e) { |
| hasErrors = true; |
| } |
| } |
| } |
| } |
| |
| private final class Entry { |
| private final String key; |
| |
| /** Lengths of this entry's files. */ |
| private final long[] lengths; |
| |
| /** True if this entry has ever been published. */ |
| private boolean readable; |
| |
| /** The ongoing edit or null if this entry is not being edited. */ |
| private Editor currentEditor; |
| |
| /** The sequence number of the most recently committed edit to this entry. */ |
| private long sequenceNumber; |
| |
| private Entry(String key) { |
| this.key = key; |
| this.lengths = new long[valueCount]; |
| } |
| |
| public String getLengths() throws IOException { |
| StringBuilder result = new StringBuilder(); |
| for (long size : lengths) { |
| result.append(' ').append(size); |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * Set lengths using decimal numbers like "10123". |
| */ |
| private void setLengths(String[] strings) throws IOException { |
| if (strings.length != valueCount) { |
| throw invalidLengths(strings); |
| } |
| |
| try { |
| for (int i = 0; i < strings.length; i++) { |
| lengths[i] = Long.parseLong(strings[i]); |
| } |
| } catch (NumberFormatException e) { |
| throw invalidLengths(strings); |
| } |
| } |
| |
| private IOException invalidLengths(String[] strings) throws IOException { |
| throw new IOException("unexpected journal line: " + Arrays.toString(strings)); |
| } |
| |
| public File getCleanFile(int i) { |
| return new File(directory, key + "." + i); |
| } |
| |
| public File getDirtyFile(int i) { |
| return new File(directory, key + "." + i + ".tmp"); |
| } |
| } |
| } |