001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.io.output; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.util.Objects; 026import java.util.function.Supplier; 027 028import org.apache.commons.io.build.AbstractStreamBuilder; 029import org.apache.commons.io.file.PathUtils; 030 031/** 032 * An output stream which will retain data in memory until a specified threshold is reached, and only then commit it to disk. If the stream is closed before the 033 * threshold is reached, the data will not be written to disk at all. 034 * <p> 035 * To build an instance, use {@link Builder}. 036 * </p> 037 * <p> 038 * The caller is responsible for deleting the output file ({@link #getFile()}, {@link #getPath()}) created by a DeferredFileOutputStream when the caller only 039 * configured a prefix. 040 * </p> 041 * <p> 042 * The caller is responsible for deleting the output file passed to a constructor or builder through {@link Builder#setOutputFile(File)} or 043 * {@link Builder#setOutputFile(Path)}. 044 * </p> 045 * <p> 046 * This class originated in FileUpload processing. In this use case, you do not know in advance the size of the file being uploaded. If the file is small you 047 * want to store it in memory (for speed), but if the file is large you want to store it to file (to avoid memory issues). 048 * </p> 049 * 050 * @see Builder 051 */ 052public class DeferredFileOutputStream extends ThresholdingOutputStream { 053 054 // @formatter:off 055 /** 056 * Builds a new {@link DeferredFileOutputStream}. 057 * <p> 058 * For example: 059 * </p> 060 * <pre>{@code 061 * DeferredFileOutputStream s = DeferredFileOutputStream.builder() 062 * .setBufferSize(4096) 063 * .setDirectory(dir) 064 * .setOutputFile(outputFile) 065 * .setPrefix(prefix) 066 * .setSuffix(suffix) 067 * .setThreshold(threshold) 068 * .get();} 069 * </pre> 070 * <p> 071 * The only super's aspect used is buffer size. 072 * </p> 073 * 074 * @see #get() 075 * @since 2.12.0 076 */ 077 // @formatter:on 078 public static class Builder extends AbstractStreamBuilder<DeferredFileOutputStream, Builder> { 079 080 private int threshold; 081 private Path outputFile; 082 private String prefix; 083 private String suffix; 084 private Path directory; 085 086 /** 087 * Constructs a new builder. 088 */ 089 public Builder() { 090 setBufferSizeDefault(AbstractByteArrayOutputStream.DEFAULT_SIZE); 091 setBufferSize(AbstractByteArrayOutputStream.DEFAULT_SIZE); 092 } 093 094 /** 095 * Builds a new {@link DeferredFileOutputStream}. 096 * <p> 097 * This builder use the following aspects: 098 * </p> 099 * <ul> 100 * <li>{@link #getBufferSize()}</li> 101 * <li>threshold</li> 102 * <li>outputFile</li> 103 * <li>prefix</li> 104 * <li>suffix</li> 105 * <li>directory</li> 106 * </ul> 107 * 108 * @return a new instance. 109 */ 110 @Override 111 public DeferredFileOutputStream get() { 112 return new DeferredFileOutputStream(threshold, outputFile, prefix, suffix, directory, getBufferSize()); 113 } 114 115 /** 116 * Sets the temporary file directory. 117 * 118 * @param directory Temporary file directory. 119 * @return {@code this} instance. 120 */ 121 public Builder setDirectory(final File directory) { 122 this.directory = toPath(directory, null); 123 return this; 124 } 125 126 /** 127 * Sets the temporary file directory. 128 * 129 * @param directory Temporary file directory. 130 * @return {@code this} instance. 131 * @since 2.14.0 132 */ 133 public Builder setDirectory(final Path directory) { 134 this.directory = toPath(directory, null); 135 return this; 136 } 137 138 /** 139 * Sets the file to which data is saved beyond the threshold. 140 * 141 * @param outputFile The file to which data is saved beyond the threshold. 142 * @return {@code this} instance. 143 */ 144 public Builder setOutputFile(final File outputFile) { 145 this.outputFile = toPath(outputFile, null); 146 return this; 147 } 148 149 /** 150 * Sets the file to which data is saved beyond the threshold. 151 * 152 * @param outputFile The file to which data is saved beyond the threshold. 153 * @return {@code this} instance. 154 * @since 2.14.0 155 */ 156 public Builder setOutputFile(final Path outputFile) { 157 this.outputFile = toPath(outputFile, null); 158 return this; 159 } 160 161 /** 162 * Sets the prefix to use for the temporary file. 163 * 164 * @param prefix Prefix to use for the temporary file. 165 * @return {@code this} instance. 166 */ 167 public Builder setPrefix(final String prefix) { 168 this.prefix = prefix; 169 return this; 170 } 171 172 /** 173 * Sets the suffix to use for the temporary file. 174 * 175 * @param suffix Suffix to use for the temporary file. 176 * @return {@code this} instance. 177 */ 178 public Builder setSuffix(final String suffix) { 179 this.suffix = suffix; 180 return this; 181 } 182 183 /** 184 * Sets the number of bytes at which to trigger an event. 185 * 186 * @param threshold The number of bytes at which to trigger an event. 187 * @return {@code this} instance. 188 */ 189 public Builder setThreshold(final int threshold) { 190 this.threshold = threshold; 191 return this; 192 } 193 194 } 195 196 /** 197 * Constructs a new {@link Builder}. 198 * 199 * @return a new {@link Builder}. 200 * @since 2.12.0 201 */ 202 public static Builder builder() { 203 return new Builder(); 204 } 205 206 private static int checkBufferSize(final int initialBufferSize) { 207 if (initialBufferSize < 0) { 208 throw new IllegalArgumentException("Initial buffer size must be at least 0."); 209 } 210 return initialBufferSize; 211 } 212 213 private static Path toPath(final File file, final Supplier<Path> defaultPathSupplier) { 214 return file != null ? file.toPath() : defaultPathSupplier == null ? null : defaultPathSupplier.get(); 215 } 216 217 private static Path toPath(final Path file, final Supplier<Path> defaultPathSupplier) { 218 return file != null ? file : defaultPathSupplier == null ? null : defaultPathSupplier.get(); 219 } 220 221 /** 222 * The output stream to which data will be written prior to the threshold being reached. 223 */ 224 private ByteArrayOutputStream memoryOutputStream; 225 226 /** 227 * The output stream to which data will be written at any given time. This will always be one of {@code memoryOutputStream} or {@code diskOutputStream}. 228 */ 229 private OutputStream currentOutputStream; 230 231 /** 232 * The file to which output will be directed if the threshold is exceeded. 233 */ 234 private Path outputPath; 235 236 /** 237 * The temporary file prefix. 238 */ 239 private final String prefix; 240 241 /** 242 * The temporary file suffix. 243 */ 244 private final String suffix; 245 246 /** 247 * The directory to use for temporary files. 248 */ 249 private final Path directory; 250 251 /** 252 * True when close() has been called successfully. 253 */ 254 private boolean closed; 255 256 /** 257 * Constructs an instance of this class which will trigger an event at the specified threshold, and save data to a file beyond that point. The initial 258 * buffer size will default to {@value AbstractByteArrayOutputStream#DEFAULT_SIZE} bytes which is ByteArrayOutputStream's default buffer size. 259 * 260 * @param threshold The number of bytes at which to trigger an event. 261 * @param outputFile The file to which data is saved beyond the threshold. 262 * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} 263 */ 264 @Deprecated 265 public DeferredFileOutputStream(final int threshold, final File outputFile) { 266 this(threshold, outputFile, null, null, null, AbstractByteArrayOutputStream.DEFAULT_SIZE); 267 } 268 269 /** 270 * Constructs an instance of this class which will trigger an event at the specified threshold, and save data either to a file beyond that point. 271 * 272 * @param threshold The number of bytes at which to trigger an event. 273 * @param outputFile The file to which data is saved beyond the threshold. 274 * @param prefix Prefix to use for the temporary file. 275 * @param suffix Suffix to use for the temporary file. 276 * @param directory Temporary file directory. 277 * @param initialBufferSize The initial size of the in memory buffer. 278 * @throws IllegalArgumentException if initialBufferSize < 0. 279 */ 280 private DeferredFileOutputStream(final int threshold, final File outputFile, final String prefix, final String suffix, final File directory, 281 final int initialBufferSize) { 282 super(threshold); 283 this.outputPath = toPath(outputFile, null); 284 this.prefix = prefix; 285 this.suffix = suffix; 286 this.directory = toPath(directory, PathUtils::getTempDirectory); 287 this.memoryOutputStream = new ByteArrayOutputStream(checkBufferSize(initialBufferSize)); 288 this.currentOutputStream = memoryOutputStream; 289 } 290 291 /** 292 * Constructs an instance of this class which will trigger an event at the specified threshold, and save data to a file beyond that point. 293 * 294 * @param threshold The number of bytes at which to trigger an event. 295 * @param initialBufferSize The initial size of the in memory buffer. 296 * @param outputFile The file to which data is saved beyond the threshold. 297 * @since 2.5 298 * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} 299 */ 300 @Deprecated 301 public DeferredFileOutputStream(final int threshold, final int initialBufferSize, final File outputFile) { 302 this(threshold, outputFile, null, null, null, initialBufferSize); 303 } 304 305 /** 306 * Constructs an instance of this class which will trigger an event at the specified threshold, and save data to a temporary file beyond that point. 307 * 308 * @param threshold The number of bytes at which to trigger an event. 309 * @param initialBufferSize The initial size of the in memory buffer. 310 * @param prefix Prefix to use for the temporary file. 311 * @param suffix Suffix to use for the temporary file. 312 * @param directory Temporary file directory. 313 * @since 2.5 314 * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} 315 */ 316 @Deprecated 317 public DeferredFileOutputStream(final int threshold, final int initialBufferSize, final String prefix, final String suffix, final File directory) { 318 this(threshold, null, Objects.requireNonNull(prefix, "prefix"), suffix, directory, initialBufferSize); 319 } 320 321 /** 322 * Constructs an instance of this class which will trigger an event at the specified threshold, and save data either to a file beyond that point. 323 * 324 * @param threshold The number of bytes at which to trigger an event. 325 * @param outputFile The file to which data is saved beyond the threshold. 326 * @param prefix Prefix to use for the temporary file. 327 * @param suffix Suffix to use for the temporary file. 328 * @param directory Temporary file directory. 329 * @param initialBufferSize The initial size of the in memory buffer. 330 * @throws IllegalArgumentException if initialBufferSize < 0. 331 */ 332 private DeferredFileOutputStream(final int threshold, final Path outputFile, final String prefix, final String suffix, final Path directory, 333 final int initialBufferSize) { 334 super(threshold); 335 this.outputPath = toPath(outputFile, null); 336 this.prefix = prefix; 337 this.suffix = suffix; 338 this.directory = toPath(directory, PathUtils::getTempDirectory); 339 this.memoryOutputStream = new ByteArrayOutputStream(checkBufferSize(initialBufferSize)); 340 this.currentOutputStream = memoryOutputStream; 341 } 342 343 /** 344 * Constructs an instance of this class which will trigger an event at the specified threshold, and save data to a temporary file beyond that point. The 345 * initial buffer size will default to 32 bytes which is ByteArrayOutputStream's default buffer size. 346 * 347 * @param threshold The number of bytes at which to trigger an event. 348 * @param prefix Prefix to use for the temporary file. 349 * @param suffix Suffix to use for the temporary file. 350 * @param directory Temporary file directory. 351 * @since 1.4 352 * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} 353 */ 354 @Deprecated 355 public DeferredFileOutputStream(final int threshold, final String prefix, final String suffix, final File directory) { 356 this(threshold, null, Objects.requireNonNull(prefix, "prefix"), suffix, directory, AbstractByteArrayOutputStream.DEFAULT_SIZE); 357 } 358 359 /** 360 * Closes underlying output stream, and mark this as closed 361 * 362 * @throws IOException if an error occurs. 363 */ 364 @Override 365 public void close() throws IOException { 366 super.close(); 367 closed = true; 368 } 369 370 /** 371 * Gets the data for this output stream as an array of bytes, assuming that the data has been retained in memory. If the data was written to disk, this 372 * method returns {@code null}. 373 * 374 * @return The data for this output stream, or {@code null} if no such data is available. 375 */ 376 public byte[] getData() { 377 return memoryOutputStream != null ? memoryOutputStream.toByteArray() : null; 378 } 379 380 /** 381 * Gets either the output File specified in the constructor or the temporary File created or null. 382 * <p> 383 * If the constructor specifying the File is used then it returns that same output File, even when threshold has not been reached. 384 * </p> 385 * <p> 386 * If constructor specifying a temporary File prefix/suffix is used then the temporary File created once the threshold is reached is returned if the 387 * threshold was not reached then {@code null} is returned. 388 * </p> 389 * 390 * @return The File for this output stream, or {@code null} if no such File exists. 391 */ 392 public File getFile() { 393 return outputPath != null ? outputPath.toFile() : null; 394 } 395 396 /** 397 * Gets either the output Path specified in the constructor or the temporary Path created or null. 398 * <p> 399 * If the constructor specifying the file is used then it returns that same output Path, even when threshold has not been reached. 400 * </p> 401 * <p> 402 * If constructor specifying a temporary Path prefix/suffix is used then the temporary Path created once the threshold is reached is returned if the 403 * threshold was not reached then {@code null} is returned. 404 * </p> 405 * 406 * @return The Path for this output stream, or {@code null} if no such Path exists. 407 * @since 2.14.0 408 */ 409 public Path getPath() { 410 return outputPath; 411 } 412 413 /** 414 * Gets the current output stream. This may be memory based or disk based, depending on the current state with respect to the threshold. 415 * 416 * @return The underlying output stream. 417 * @throws IOException if an error occurs. 418 */ 419 @Override 420 protected OutputStream getStream() throws IOException { 421 return currentOutputStream; 422 } 423 424 /** 425 * Tests whether or not the data for this output stream has been retained in memory. 426 * 427 * @return {@code true} if the data is available in memory; {@code false} otherwise. 428 */ 429 public boolean isInMemory() { 430 return !isThresholdExceeded(); 431 } 432 433 /** 434 * Switches the underlying output stream from a memory based stream to one that is backed by disk. This is the point at which we realize that too much data 435 * is being written to keep in memory, so we elect to switch to disk-based storage. 436 * 437 * @throws IOException if an error occurs. 438 */ 439 @Override 440 protected void thresholdReached() throws IOException { 441 if (prefix != null) { 442 outputPath = Files.createTempFile(directory, prefix, suffix); 443 } 444 PathUtils.createParentDirectories(outputPath, null, PathUtils.EMPTY_FILE_ATTRIBUTE_ARRAY); 445 final OutputStream fos = Files.newOutputStream(outputPath); 446 try { 447 memoryOutputStream.writeTo(fos); 448 } catch (final IOException e) { 449 fos.close(); 450 throw e; 451 } 452 currentOutputStream = fos; 453 memoryOutputStream = null; 454 } 455 456 /** 457 * Converts the current contents of this byte stream to an {@link InputStream}. If the data for this output stream has been retained in memory, the returned 458 * stream is backed by buffers of {@code this} stream, avoiding memory allocation and copy, thus saving space and time.<br> 459 * Otherwise, the returned stream will be one that is created from the data that has been committed to disk. 460 * 461 * @return the current contents of this output stream. 462 * @throws IOException if this stream is not yet closed or an error occurs. 463 * @see org.apache.commons.io.output.ByteArrayOutputStream#toInputStream() 464 * 465 * @since 2.9.0 466 */ 467 public InputStream toInputStream() throws IOException { 468 // we may only need to check if this is closed if we are working with a file 469 // but we should force the habit of closing whether we are working with 470 // a file or memory. 471 if (!closed) { 472 throw new IOException("Stream not closed"); 473 } 474 if (isInMemory()) { 475 return memoryOutputStream.toInputStream(); 476 } 477 return Files.newInputStream(outputPath); 478 } 479 480 /** 481 * Writes the data from this output stream to the specified output stream, after it has been closed. 482 * 483 * @param outputStream output stream to write to. 484 * @throws NullPointerException if the OutputStream is {@code null}. 485 * @throws IOException if this stream is not yet closed or an error occurs. 486 */ 487 public void writeTo(final OutputStream outputStream) throws IOException { 488 // we may only need to check if this is closed if we are working with a file 489 // but we should force the habit of closing whether we are working with 490 // a file or memory. 491 if (!closed) { 492 throw new IOException("Stream not closed"); 493 } 494 if (isInMemory()) { 495 memoryOutputStream.writeTo(outputStream); 496 } else { 497 Files.copy(outputPath, outputStream); 498 } 499 } 500}