• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
getBuffer()53     byte[] getBuffer() {
54       return buf;
55     }
56 
getCount()57     int getCount() {
58       return count;
59     }
60   }
61 
62   /** Returns the file holding the data (possibly null). */
getFile()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    */
FileBackedOutputStream(int fileThreshold)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    */
FileBackedOutputStream(int fileThreshold, boolean resetOnFinalize)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    */
asByteSource()127   public ByteSource asByteSource() {
128     return source;
129   }
130 
openInputStream()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    */
reset()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 
write(int b)167   @Override public synchronized void write(int b) throws IOException {
168     update(1);
169     out.write(b);
170   }
171 
write(byte[] b)172   @Override public synchronized void write(byte[] b) throws IOException {
173     write(b, 0, b.length);
174   }
175 
write(byte[] b, int off, int len)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 
close()182   @Override public synchronized void close() throws IOException {
183     out.close();
184   }
185 
flush()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    */
update(int len)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