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