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 InputSupplier<InputStream> supplier;
     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 InputSupplier} returned by
     70    * {@link #getSupplier} 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 InputSupplier} returned
     82    * by {@link #getSupplier} 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 InputSupplier} returned by {@link
     88    *     #getSupplier} 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       supplier = new InputSupplier<InputStream>() {
     98         @Override
     99         public InputStream getInput() throws IOException {
    100           return openStream();
    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       supplier = new InputSupplier<InputStream>() {
    113         @Override
    114         public InputStream getInput() throws IOException {
    115           return openStream();
    116         }
    117       };
    118     }
    119   }
    120 
    121   /**
    122    * Returns a supplier that may be used to retrieve the data buffered
    123    * by this stream.
    124    */
    125   public InputSupplier<InputStream> getSupplier() {
    126     return supplier;
    127   }
    128 
    129   private synchronized InputStream openStream() throws IOException {
    130     if (file != null) {
    131       return new FileInputStream(file);
    132     } else {
    133       return new ByteArrayInputStream(
    134           memory.getBuffer(), 0, memory.getCount());
    135     }
    136   }
    137 
    138   /**
    139    * Calls {@link #close} if not already closed, and then resets this
    140    * object back to its initial state, for reuse. If data was buffered
    141    * to a file, it will be deleted.
    142    *
    143    * @throws IOException if an I/O error occurred while deleting the file buffer
    144    */
    145   public synchronized void reset() throws IOException {
    146     try {
    147       close();
    148     } finally {
    149       if (memory == null) {
    150         memory = new MemoryOutput();
    151       } else {
    152         memory.reset();
    153       }
    154       out = memory;
    155       if (file != null) {
    156         File deleteMe = file;
    157         file = null;
    158         if (!deleteMe.delete()) {
    159           throw new IOException("Could not delete: " + deleteMe);
    160         }
    161       }
    162     }
    163   }
    164 
    165   @Override public synchronized void write(int b) throws IOException {
    166     update(1);
    167     out.write(b);
    168   }
    169 
    170   @Override public synchronized void write(byte[] b) throws IOException {
    171     write(b, 0, b.length);
    172   }
    173 
    174   @Override public synchronized void write(byte[] b, int off, int len)
    175       throws IOException {
    176     update(len);
    177     out.write(b, off, len);
    178   }
    179 
    180   @Override public synchronized void close() throws IOException {
    181     out.close();
    182   }
    183 
    184   @Override public synchronized void flush() throws IOException {
    185     out.flush();
    186   }
    187 
    188   /**
    189    * Checks if writing {@code len} bytes would go over threshold, and
    190    * switches to file buffering if so.
    191    */
    192   private void update(int len) throws IOException {
    193     if (file == null && (memory.getCount() + len > fileThreshold)) {
    194       File temp = File.createTempFile("FileBackedOutputStream", null);
    195       if (resetOnFinalize) {
    196         // Finalizers are not guaranteed to be called on system shutdown;
    197         // this is insurance.
    198         temp.deleteOnExit();
    199       }
    200       FileOutputStream transfer = new FileOutputStream(temp);
    201       transfer.write(memory.getBuffer(), 0, memory.getCount());
    202       transfer.flush();
    203 
    204       // We've successfully transferred the data; switch to writing to file
    205       out = transfer;
    206       file = temp;
    207       memory = null;
    208     }
    209   }
    210 }
    211