/*
* 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.VisibleForTesting;
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;
/**
* An {@link OutputStream} that starts buffering to a byte array, but
* switches to file buffering once the data reaches a configurable size.
*
*
This class is thread-safe.
*
* @author Chris Nokleberg
* @since 1.0
*/
@Beta
public final class FileBackedOutputStream extends OutputStream {
private final int fileThreshold;
private final boolean resetOnFinalize;
private final InputSupplier supplier;
private OutputStream out;
private MemoryOutput memory;
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 InputSupplier} returned by
* {@link #getSupplier} 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 InputSupplier} returned
* by {@link #getSupplier} 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 InputSupplier} returned by {@link
* #getSupplier} is finalized
*/
public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) {
this.fileThreshold = fileThreshold;
this.resetOnFinalize = resetOnFinalize;
memory = new MemoryOutput();
out = memory;
if (resetOnFinalize) {
supplier = new InputSupplier() {
@Override
public InputStream getInput() throws IOException {
return openStream();
}
@Override protected void finalize() {
try {
reset();
} catch (Throwable t) {
t.printStackTrace(System.err);
}
}
};
} else {
supplier = new InputSupplier() {
@Override
public InputStream getInput() throws IOException {
return openStream();
}
};
}
}
/**
* Returns a supplier that may be used to retrieve the data buffered
* by this stream.
*/
public InputSupplier getSupplier() {
return supplier;
}
private synchronized InputStream openStream() 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.
*/
private void update(int len) throws IOException {
if (file == null && (memory.getCount() + len > fileThreshold)) {
File temp = File.createTempFile("FileBackedOutputStream", null);
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;
}
}
}