/* * 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. * * <p>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 ByteSource source; 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 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 = fileThreshold; this.resetOnFinalize = resetOnFinalize; 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. */ 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; } } }