001/*
002 * Copyright (C) 2008 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package com.google.common.io;
016
017import com.google.common.annotations.Beta;
018import com.google.common.annotations.GwtIncompatible;
019import com.google.common.annotations.VisibleForTesting;
020import com.google.errorprone.annotations.concurrent.GuardedBy;
021import java.io.ByteArrayInputStream;
022import java.io.ByteArrayOutputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FileOutputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import org.checkerframework.checker.nullness.compatqual.NullableDecl;
030
031/**
032 * An {@link OutputStream} that starts buffering to a byte array, but switches to file buffering
033 * once the data reaches a configurable size.
034 *
035 * <p>Temporary files created by this stream may live in the local filesystem until either:
036 *
037 * <ul>
038 *   <li>{@link #reset} is called (removing the data in this stream and deleting the file), or...
039 *   <li>this stream (or, more precisely, its {@link #asByteSource} view) is finalized during
040 *       garbage collection, <strong>AND</strong> this stream was not constructed with {@linkplain
041 *       #FileBackedOutputStream(int) the 1-arg constructor} or the {@linkplain
042 *       #FileBackedOutputStream(int, boolean) 2-arg constructor} passing {@code false} in the
043 *       second parameter.
044 * </ul>
045 *
046 * <p>This class is thread-safe.
047 *
048 * @author Chris Nokleberg
049 * @since 1.0
050 */
051@Beta
052@GwtIncompatible
053public final class FileBackedOutputStream extends OutputStream {
054  private final int fileThreshold;
055  private final boolean resetOnFinalize;
056  private final ByteSource source;
057  @NullableDecl private final File parentDirectory;
058
059  @GuardedBy("this")
060  private OutputStream out;
061
062  @GuardedBy("this")
063  private MemoryOutput memory;
064
065  @GuardedBy("this")
066  @NullableDecl
067  private File file;
068
069  /** ByteArrayOutputStream that exposes its internals. */
070  private static class MemoryOutput extends ByteArrayOutputStream {
071    byte[] getBuffer() {
072      return buf;
073    }
074
075    int getCount() {
076      return count;
077    }
078  }
079
080  /** Returns the file holding the data (possibly null). */
081  @VisibleForTesting
082  synchronized File getFile() {
083    return file;
084  }
085
086  /**
087   * Creates a new instance that uses the given file threshold, and does not reset the data when the
088   * {@link ByteSource} returned by {@link #asByteSource} is finalized.
089   *
090   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
091   */
092  public FileBackedOutputStream(int fileThreshold) {
093    this(fileThreshold, false);
094  }
095
096  /**
097   * Creates a new instance that uses the given file threshold, and optionally resets the data when
098   * the {@link ByteSource} returned by {@link #asByteSource} is finalized.
099   *
100   * @param fileThreshold the number of bytes before the stream should switch to buffering to a file
101   * @param resetOnFinalize if true, the {@link #reset} method will be called when the {@link
102   *     ByteSource} returned by {@link #asByteSource} is finalized.
103   */
104  public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) {
105    this(fileThreshold, resetOnFinalize, null);
106  }
107
108  private FileBackedOutputStream(
109      int fileThreshold, boolean resetOnFinalize, @NullableDecl File parentDirectory) {
110    this.fileThreshold = fileThreshold;
111    this.resetOnFinalize = resetOnFinalize;
112    this.parentDirectory = parentDirectory;
113    memory = new MemoryOutput();
114    out = memory;
115
116    if (resetOnFinalize) {
117      source =
118          new ByteSource() {
119            @Override
120            public InputStream openStream() throws IOException {
121              return openInputStream();
122            }
123
124            @Override
125            protected void finalize() {
126              try {
127                reset();
128              } catch (Throwable t) {
129                t.printStackTrace(System.err);
130              }
131            }
132          };
133    } else {
134      source =
135          new ByteSource() {
136            @Override
137            public InputStream openStream() throws IOException {
138              return openInputStream();
139            }
140          };
141    }
142  }
143
144  /**
145   * Returns a readable {@link ByteSource} view of the data that has been written to this stream.
146   *
147   * @since 15.0
148   */
149  public ByteSource asByteSource() {
150    return source;
151  }
152
153  private synchronized InputStream openInputStream() throws IOException {
154    if (file != null) {
155      return new FileInputStream(file);
156    } else {
157      return new ByteArrayInputStream(memory.getBuffer(), 0, memory.getCount());
158    }
159  }
160
161  /**
162   * Calls {@link #close} if not already closed, and then resets this object back to its initial
163   * state, for reuse. If data was buffered to a file, it will be deleted.
164   *
165   * @throws IOException if an I/O error occurred while deleting the file buffer
166   */
167  public synchronized void reset() throws IOException {
168    try {
169      close();
170    } finally {
171      if (memory == null) {
172        memory = new MemoryOutput();
173      } else {
174        memory.reset();
175      }
176      out = memory;
177      if (file != null) {
178        File deleteMe = file;
179        file = null;
180        if (!deleteMe.delete()) {
181          throw new IOException("Could not delete: " + deleteMe);
182        }
183      }
184    }
185  }
186
187  @Override
188  public synchronized void write(int b) throws IOException {
189    update(1);
190    out.write(b);
191  }
192
193  @Override
194  public synchronized void write(byte[] b) throws IOException {
195    write(b, 0, b.length);
196  }
197
198  @Override
199  public synchronized void write(byte[] b, int off, int len) throws IOException {
200    update(len);
201    out.write(b, off, len);
202  }
203
204  @Override
205  public synchronized void close() throws IOException {
206    out.close();
207  }
208
209  @Override
210  public synchronized void flush() throws IOException {
211    out.flush();
212  }
213
214  /**
215   * Checks if writing {@code len} bytes would go over threshold, and switches to file buffering if
216   * so.
217   */
218  @GuardedBy("this")
219  private void update(int len) throws IOException {
220    if (file == null && (memory.getCount() + len > fileThreshold)) {
221      File temp = File.createTempFile("FileBackedOutputStream", null, parentDirectory);
222      if (resetOnFinalize) {
223        // Finalizers are not guaranteed to be called on system shutdown;
224        // this is insurance.
225        temp.deleteOnExit();
226      }
227      FileOutputStream transfer = new FileOutputStream(temp);
228      transfer.write(memory.getBuffer(), 0, memory.getCount());
229      transfer.flush();
230
231      // We've successfully transferred the data; switch to writing to file
232      out = transfer;
233      file = temp;
234      memory = null;
235    }
236  }
237}