Home | History | Annotate | Download | only in io
      1 /*
      2  * Copyright (C) 2008 The Guava Authors
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.common.io;
     18 
     19 import com.google.common.annotations.Beta;
     20 import com.google.common.annotations.VisibleForTesting;
     21 
     22 import java.io.ByteArrayInputStream;
     23 import java.io.ByteArrayOutputStream;
     24 import java.io.File;
     25 import java.io.FileInputStream;
     26 import java.io.FileOutputStream;
     27 import java.io.IOException;
     28 import java.io.InputStream;
     29 import java.io.OutputStream;
     30 
     31 /**
     32  * An {@link OutputStream} that starts buffering to a byte array, but
     33  * switches to file buffering once the data reaches a configurable size.
     34  *
     35  * <p>This class is thread-safe.
     36  *
     37  * @author Chris Nokleberg
     38  * @since 1.0
     39  */
     40 @Beta
     41 public final class FileBackedOutputStream extends OutputStream {
     42 
     43   private final int fileThreshold;
     44   private final boolean resetOnFinalize;
     45   private final ByteSource source;
     46 
     47   private OutputStream out;
     48   private MemoryOutput memory;
     49   private File file;
     50 
     51   /** ByteArrayOutputStream that exposes its internals. */
     52   private static class MemoryOutput extends ByteArrayOutputStream {
     53     byte[] getBuffer() {
     54       return buf;
     55     }
     56 
     57     int getCount() {
     58       return count;
     59     }
     60   }
     61 
     62   /** Returns the file holding the data (possibly null). */
     63   @VisibleForTesting synchronized File getFile() {
     64     return file;
     65   }
     66 
     67   /**
     68    * Creates a new instance that uses the given file threshold, and does
     69    * not reset the data when the {@link ByteSource} returned by
     70    * {@link #asByteSource} is finalized.
     71    *
     72    * @param fileThreshold the number of bytes before the stream should
     73    *     switch to buffering to a file
     74    */
     75   public FileBackedOutputStream(int fileThreshold) {
     76     this(fileThreshold, false);
     77   }
     78 
     79   /**
     80    * Creates a new instance that uses the given file threshold, and
     81    * optionally resets the data when the {@link ByteSource} returned
     82    * by {@link #asByteSource} is finalized.
     83    *
     84    * @param fileThreshold the number of bytes before the stream should
     85    *     switch to buffering to a file
     86    * @param resetOnFinalize if true, the {@link #reset} method will
     87    *     be called when the {@link ByteSource} returned by {@link
     88    *     #asByteSource} is finalized
     89    */
     90   public FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize) {
     91     this.fileThreshold = fileThreshold;
     92     this.resetOnFinalize = resetOnFinalize;
     93     memory = new MemoryOutput();
     94     out = memory;
     95 
     96     if (resetOnFinalize) {
     97       source = new ByteSource() {
     98         @Override
     99         public InputStream openStream() throws IOException {
    100           return openInputStream();
    101         }
    102 
    103         @Override protected void finalize() {
    104           try {
    105             reset();
    106           } catch (Throwable t) {
    107             t.printStackTrace(System.err);
    108           }
    109         }
    110       };
    111     } else {
    112       source = new ByteSource() {
    113         @Override
    114         public InputStream openStream() throws IOException {
    115           return openInputStream();
    116         }
    117       };
    118     }
    119   }
    120 
    121   /**
    122    * Returns a readable {@link ByteSource} view of the data that has been
    123    * written to this stream.
    124    *
    125    * @since 15.0
    126    */
    127   public ByteSource asByteSource() {
    128     return source;
    129   }
    130 
    131   private synchronized InputStream openInputStream() throws IOException {
    132     if (file != null) {
    133       return new FileInputStream(file);
    134     } else {
    135       return new ByteArrayInputStream(
    136           memory.getBuffer(), 0, memory.getCount());
    137     }
    138   }
    139 
    140   /**
    141    * Calls {@link #close} if not already closed, and then resets this
    142    * object back to its initial state, for reuse. If data was buffered
    143    * to a file, it will be deleted.
    144    *
    145    * @throws IOException if an I/O error occurred while deleting the file buffer
    146    */
    147   public synchronized void reset() throws IOException {
    148     try {
    149       close();
    150     } finally {
    151       if (memory == null) {
    152         memory = new MemoryOutput();
    153       } else {
    154         memory.reset();
    155       }
    156       out = memory;
    157       if (file != null) {
    158         File deleteMe = file;
    159         file = null;
    160         if (!deleteMe.delete()) {
    161           throw new IOException("Could not delete: " + deleteMe);
    162         }
    163       }
    164     }
    165   }
    166 
    167   @Override public synchronized void write(int b) throws IOException {
    168     update(1);
    169     out.write(b);
    170   }
    171 
    172   @Override public synchronized void write(byte[] b) throws IOException {
    173     write(b, 0, b.length);
    174   }
    175 
    176   @Override public synchronized void write(byte[] b, int off, int len)
    177       throws IOException {
    178     update(len);
    179     out.write(b, off, len);
    180   }
    181 
    182   @Override public synchronized void close() throws IOException {
    183     out.close();
    184   }
    185 
    186   @Override public synchronized void flush() throws IOException {
    187     out.flush();
    188   }
    189 
    190   /**
    191    * Checks if writing {@code len} bytes would go over threshold, and
    192    * switches to file buffering if so.
    193    */
    194   private void update(int len) throws IOException {
    195     if (file == null && (memory.getCount() + len > fileThreshold)) {
    196       File temp = File.createTempFile("FileBackedOutputStream", null);
    197       if (resetOnFinalize) {
    198         // Finalizers are not guaranteed to be called on system shutdown;
    199         // this is insurance.
    200         temp.deleteOnExit();
    201       }
    202       FileOutputStream transfer = new FileOutputStream(temp);
    203       transfer.write(memory.getBuffer(), 0, memory.getCount());
    204       transfer.flush();
    205 
    206       // We've successfully transferred the data; switch to writing to file
    207       out = transfer;
    208       file = temp;
    209       memory = null;
    210     }
    211   }
    212 }
    213