blob: b72c40e2ddae3a61fa4e3a8a3e3a3c80f388550a [file] [log] [blame]
/*
* Copyright (C) 2008 The Guava Authors
*
* 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.google.common.io;
import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
/**
* An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering
* once the data reaches a configurable size.
*
* <p>Temporary files created by this stream may live in the local filesystem until either:
*
* <ul>
* <li>{@link #reset} is called (removing the data in this stream and deleting the file), or...
* <li>this stream (or, more precisely, its {@link #asByteSource} view) is finalized during
* garbage collection, <strong>AND</strong> this stream was not constructed with {@linkplain
* #FileBackedOutputStream(int) the 1-arg constructor} or the {@linkplain
* #FileBackedOutputStream(int, boolean) 2-arg constructor} passing {@code false} in the
* second parameter.
* </ul>
*
* <p>This class is thread-safe.
*
* @author Chris Nokleberg
* @since 1.0
*/
@Beta
@GwtIncompatible
public final class FileBackedOutputStream extends OutputStream {
private final int fileThreshold;
private final boolean resetOnFinalize;
private final ByteSource source;
@NullableDecl private final File parentDirectory;
@GuardedBy("this")
private OutputStream out;
@GuardedBy("this")
private MemoryOutput memory;
@GuardedBy("this")
@NullableDecl
private File file;
/** ByteArrayOutputStream that exposes its internals. */
private static class MemoryOutput extends ByteArrayOutputStream {
byte[] getBuffer() {
return buf;
}
int getCount() {
return count;
}
}
/** Returns the file holding the data (possibly null). */
@VisibleForTesting
synchronized File getFile() {
return file;
}
/**
* Creates a new instance that uses the given file threshold, and does not reset the data when the
* {@link ByteSource} returned by {@link #asByteSource} is finalized.
*
* @param fileThreshold the number of bytes before the stream should switch to buffering to a file
*/
public FileBackedOutputStream(int fileThreshold) {
this(fileThreshold, false);
}
/**
* Creates a new instance that uses the given file threshold, and optionally resets the data when
* the {@link ByteSource} returned by {@link #asByteSource} is finalized.
*
* @param fileThreshold the number of bytes before the stream should switch to buffering to a file
* @param resetOnFinalize if true, the {@link #reset} method will be called when the {@link
* ByteSource} returned by {@link #asByteSource} is finalized.
*/
public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) {
this(fileThreshold, resetOnFinalize, null);
}
private FileBackedOutputStream(
int fileThreshold, boolean resetOnFinalize, @NullableDecl File parentDirectory) {
this.fileThreshold = fileThreshold;
this.resetOnFinalize = resetOnFinalize;
this.parentDirectory = parentDirectory;
memory = new MemoryOutput();
out = memory;
if (resetOnFinalize) {
source =
new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return openInputStream();
}
@Override
protected void finalize() {
try {
reset();
} catch (Throwable t) {
t.printStackTrace(System.err);
}
}
};
} else {
source =
new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return openInputStream();
}
};
}
}
/**
* Returns a readable {@link ByteSource} view of the data that has been written to this stream.
*
* @since 15.0
*/
public ByteSource asByteSource() {
return source;
}
private synchronized InputStream openInputStream() throws IOException {
if (file != null) {
return new FileInputStream(file);
} else {
return new ByteArrayInputStream(memory.getBuffer(), 0, memory.getCount());
}
}
/**
* Calls {@link #close} if not already closed, and then resets this object back to its initial
* state, for reuse. If data was buffered to a file, it will be deleted.
*
* @throws IOException if an I/O error occurred while deleting the file buffer
*/
public synchronized void reset() throws IOException {
try {
close();
} finally {
if (memory == null) {
memory = new MemoryOutput();
} else {
memory.reset();
}
out = memory;
if (file != null) {
File deleteMe = file;
file = null;
if (!deleteMe.delete()) {
throw new IOException("Could not delete: " + deleteMe);
}
}
}
}
@Override
public synchronized void write(int b) throws IOException {
update(1);
out.write(b);
}
@Override
public synchronized void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
update(len);
out.write(b, off, len);
}
@Override
public synchronized void close() throws IOException {
out.close();
}
@Override
public synchronized void flush() throws IOException {
out.flush();
}
/**
* Checks if writing {@code len} bytes would go over threshold, and switches to file buffering if
* so.
*/
@GuardedBy("this")
private void update(int len) throws IOException {
if (file == null && (memory.getCount() + len > fileThreshold)) {
File temp = File.createTempFile("FileBackedOutputStream", null, parentDirectory);
if (resetOnFinalize) {
// Finalizers are not guaranteed to be called on system shutdown;
// this is insurance.
temp.deleteOnExit();
}
FileOutputStream transfer = new FileOutputStream(temp);
transfer.write(memory.getBuffer(), 0, memory.getCount());
transfer.flush();
// We've successfully transferred the data; switch to writing to file
out = transfer;
file = temp;
memory = null;
}
}
}