View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io.output;
18  
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  
25  import org.apache.commons.io.FileUtils;
26  import org.apache.commons.io.IOUtils;
27  
28  
29  /**
30   * An output stream which will retain data in memory until a specified
31   * threshold is reached, and only then commit it to disk. If the stream is
32   * closed before the threshold is reached, the data will not be written to
33   * disk at all.
34   * <p>
35   * This class originated in FileUpload processing. In this use case, you do
36   * not know in advance the size of the file being uploaded. If the file is small
37   * you want to store it in memory (for speed), but if the file is large you want
38   * to store it to file (to avoid memory issues).
39   *
40   */
41  public class DeferredFileOutputStream
42      extends ThresholdingOutputStream
43  {
44      // ----------------------------------------------------------- Data members
45  
46  
47      /**
48       * The output stream to which data will be written prior to the threshold
49       * being reached.
50       */
51      private ByteArrayOutputStream memoryOutputStream;
52  
53  
54      /**
55       * The output stream to which data will be written at any given time. This
56       * will always be one of <code>memoryOutputStream</code> or
57       * <code>diskOutputStream</code>.
58       */
59      private OutputStream currentOutputStream;
60  
61  
62      /**
63       * The file to which output will be directed if the threshold is exceeded.
64       */
65      private File outputFile;
66  
67      /**
68       * The temporary file prefix.
69       */
70      private final String prefix;
71  
72      /**
73       * The temporary file suffix.
74       */
75      private final String suffix;
76  
77      /**
78       * The directory to use for temporary files.
79       */
80      private final File directory;
81  
82  
83      /**
84       * True when close() has been called successfully.
85       */
86      private boolean closed = false;
87  
88      // ----------------------------------------------------------- Constructors
89  
90  
91      /**
92       * Constructs an instance of this class which will trigger an event at the
93       * specified threshold, and save data to a file beyond that point.
94       * The initial buffer size will default to 1024 bytes which is ByteArrayOutputStream's default buffer size.
95       *
96       * @param threshold  The number of bytes at which to trigger an event.
97       * @param outputFile The file to which data is saved beyond the threshold.
98       */
99      public DeferredFileOutputStream(final int threshold, final File outputFile)
100     {
101         this(threshold,  outputFile, null, null, null, ByteArrayOutputStream.DEFAULT_SIZE);
102     }
103 
104     /**
105      * Constructs an instance of this class which will trigger an event at the
106      * specified threshold, and save data to a file beyond that point.
107      *
108      * @param threshold  The number of bytes at which to trigger an event.
109      * @param initialBufferSize The initial size of the in memory buffer.
110      * @param outputFile The file to which data is saved beyond the threshold.
111      *
112      * @since 2.5
113      */
114     public DeferredFileOutputStream(final int threshold, final int initialBufferSize, final File outputFile)
115     {
116         this(threshold, outputFile, null, null, null, initialBufferSize);
117         if (initialBufferSize < 0) {
118             throw new IllegalArgumentException("Initial buffer size must be atleast 0.");
119         }
120     }
121 
122     /**
123      * Constructs an instance of this class which will trigger an event at the
124      * specified threshold, and save data to a temporary file beyond that point.
125      * The initial buffer size will default to 32 bytes which is ByteArrayOutputStream's default buffer size.
126      *
127      * @param threshold  The number of bytes at which to trigger an event.
128      * @param prefix Prefix to use for the temporary file.
129      * @param suffix Suffix to use for the temporary file.
130      * @param directory Temporary file directory.
131      *
132      * @since 1.4
133      */
134     public DeferredFileOutputStream(final int threshold, final String prefix, final String suffix, final File directory)
135     {
136         this(threshold, null, prefix, suffix, directory, ByteArrayOutputStream.DEFAULT_SIZE);
137         if (prefix == null) {
138             throw new IllegalArgumentException("Temporary file prefix is missing");
139         }
140     }
141 
142     /**
143      * Constructs an instance of this class which will trigger an event at the
144      * specified threshold, and save data to a temporary file beyond that point.
145      *
146      * @param threshold  The number of bytes at which to trigger an event.
147      * @param initialBufferSize The initial size of the in memory buffer.
148      * @param prefix Prefix to use for the temporary file.
149      * @param suffix Suffix to use for the temporary file.
150      * @param directory Temporary file directory.
151      *
152      * @since 2.5
153      */
154     public DeferredFileOutputStream(final int threshold, final int initialBufferSize, final String prefix,
155                                     final String suffix, final File directory)
156     {
157         this(threshold, null, prefix, suffix, directory, initialBufferSize);
158         if (prefix == null) {
159             throw new IllegalArgumentException("Temporary file prefix is missing");
160         }
161         if (initialBufferSize < 0) {
162             throw new IllegalArgumentException("Initial buffer size must be atleast 0.");
163         }
164     }
165 
166     /**
167      * Constructs an instance of this class which will trigger an event at the
168      * specified threshold, and save data either to a file beyond that point.
169      *
170      * @param threshold  The number of bytes at which to trigger an event.
171      * @param outputFile The file to which data is saved beyond the threshold.
172      * @param prefix Prefix to use for the temporary file.
173      * @param suffix Suffix to use for the temporary file.
174      * @param directory Temporary file directory.
175      * @param initialBufferSize The initial size of the in memory buffer.
176      */
177     private DeferredFileOutputStream(final int threshold, final File outputFile, final String prefix,
178                                      final String suffix, final File directory, final int initialBufferSize) {
179         super(threshold);
180         this.outputFile = outputFile;
181         this.prefix = prefix;
182         this.suffix = suffix;
183         this.directory = directory;
184 
185         memoryOutputStream = new ByteArrayOutputStream(initialBufferSize);
186         currentOutputStream = memoryOutputStream;
187     }
188 
189 
190     // --------------------------------------- ThresholdingOutputStream methods
191 
192 
193     /**
194      * Returns the current output stream. This may be memory based or disk
195      * based, depending on the current state with respect to the threshold.
196      *
197      * @return The underlying output stream.
198      *
199      * @throws IOException if an error occurs.
200      */
201     @Override
202     protected OutputStream getStream() throws IOException
203     {
204         return currentOutputStream;
205     }
206 
207 
208     /**
209      * Switches the underlying output stream from a memory based stream to one
210      * that is backed by disk. This is the point at which we realise that too
211      * much data is being written to keep in memory, so we elect to switch to
212      * disk-based storage.
213      *
214      * @throws IOException if an error occurs.
215      */
216     @Override
217     protected void thresholdReached() throws IOException
218     {
219         if (prefix != null) {
220             outputFile = File.createTempFile(prefix, suffix, directory);
221         }
222         FileUtils.forceMkdirParent(outputFile);
223         final FileOutputStream fos = new FileOutputStream(outputFile);
224         try {
225             memoryOutputStream.writeTo(fos);
226         } catch (IOException e){
227             fos.close();
228             throw e;
229         }
230         currentOutputStream = fos;
231         memoryOutputStream = null;
232     }
233 
234 
235     // --------------------------------------------------------- Public methods
236 
237 
238     /**
239      * Determines whether or not the data for this output stream has been
240      * retained in memory.
241      *
242      * @return {@code true} if the data is available in memory;
243      *         {@code false} otherwise.
244      */
245     public boolean isInMemory()
246     {
247         return !isThresholdExceeded();
248     }
249 
250 
251     /**
252      * Returns the data for this output stream as an array of bytes, assuming
253      * that the data has been retained in memory. If the data was written to
254      * disk, this method returns {@code null}.
255      *
256      * @return The data for this output stream, or {@code null} if no such
257      *         data is available.
258      */
259     public byte[] getData()
260     {
261         if (memoryOutputStream != null)
262         {
263             return memoryOutputStream.toByteArray();
264         }
265         return null;
266     }
267 
268 
269     /**
270      * Returns either the output file specified in the constructor or
271      * the temporary file created or null.
272      * <p>
273      * If the constructor specifying the file is used then it returns that
274      * same output file, even when threshold has not been reached.
275      * <p>
276      * If constructor specifying a temporary file prefix/suffix is used
277      * then the temporary file created once the threshold is reached is returned
278      * If the threshold was not reached then {@code null} is returned.
279      *
280      * @return The file for this output stream, or {@code null} if no such
281      *         file exists.
282      */
283     public File getFile()
284     {
285         return outputFile;
286     }
287 
288 
289     /**
290      * Closes underlying output stream, and mark this as closed
291      *
292      * @throws IOException if an error occurs.
293      */
294     @Override
295     public void close() throws IOException
296     {
297         super.close();
298         closed = true;
299     }
300 
301 
302     /**
303      * Writes the data from this output stream to the specified output stream,
304      * after it has been closed.
305      *
306      * @param out output stream to write to.
307      * @throws IOException if this stream is not yet closed or an error occurs.
308      */
309     public void writeTo(final OutputStream out) throws IOException
310     {
311         // we may only need to check if this is closed if we are working with a file
312         // but we should force the habit of closing wether we are working with
313         // a file or memory.
314         if (!closed) {
315             throw new IOException("Stream not closed");
316         }
317 
318         if (isInMemory()) {
319             memoryOutputStream.writeTo(out);
320         } else {
321             try (FileInputStream fis = new FileInputStream(outputFile)) {
322                 IOUtils.copy(fis, out);
323             }
324         }
325     }
326 }