OK, we can keep the class but I don't fully understand the difference so maybe I need to re-read the code or the docs can be clearer.
We should make it non-public to reduce the API surface and avoid possible confusion. Ty! Gary On Thu, Jul 24, 2025, 04:34 Jochen Wiedmann <jochen.wiedm...@gmail.com> wrote: > Hi, Gary, > > sorry for the late reply. I missed your question initially. > > > > > On Sun, Jul 13, 2025 at 4:17 PM Gary Gregory <garydgreg...@gmail.com> > wrote: > > > Why duplicate Commons IO's DeferredFileOutputStream? It seems better > > to reuse than to duplicate or reinvent the wheel. > > Of course, adapting the DFO from commons-io (DFOCI) was my first idea. > But, while implementing > a unit test, for the DiskFileItem, based on that, a few things became > clear to me: > > - The dependency on DFOCI is exactly, what caused the bug report > (FILEUPLOAD-295), because > the former changed it's behaviour. > - Even worse, by adapting DFOCI to suit the needs of FileUpload, I'd > be most likely change it's > behaviour again. (Which would probably trigger a revert, hence yet > another change.) > - Having a dedicated implementation (DeferrableOutputStream, DFOCF) in > our own area > with a unit test, that maintains the expected behaviour, is the best > way to ensure compliance > to a specified behaviour. And, obviously, such compliance is expected. > > I have tried to point that out in the Javadocs (probably not very > well). If you're not happy > with my approach: Feel free to revert my changes. But, if so, you > should also take up > FILEUPLOAD-295, because (according to the reporters feedback), that > can be resolved > now. > > Jochen > > P.S: Public class, or not, I don't care. > > > > > > If the new class is required, can Commons IO be improved instead to > > meet FileUpload's needs? > > > > If not, does the new class need to be public? > > > > TY! > > Gary > > > > On Sat, Jul 12, 2025 at 4:03 PM <joc...@apache.org> wrote: > > > > > > This is an automated email from the ASF dual-hosted git repository. > > > > > > jochen pushed a commit to branch master > > > in repository > https://gitbox.apache.org/repos/asf/commons-fileupload.git > > > > > > > > > The following commit(s) were added to refs/heads/master by this push: > > > new c24e594b FILEUPLOAD-295: Implementation of the > DeferrableOutputStream > > > c24e594b is described below > > > > > > commit c24e594b4973a7fad75ca320d37f97b7682ef043 > > > Author: Jochen Wiedmann <jochen.wiedm...@gmail.com> > > > AuthorDate: Sun Jul 13 01:03:30 2025 +0200 > > > > > > FILEUPLOAD-295: Implementation of the DeferrableOutputStream > > > --- > > > .../fileupload2/core/DeferrableOutputStream.java | 369 > +++++++++++++++++++++ > > > .../core/DeferrableOutputStreamTest.java | 235 +++++++++++++ > > > 2 files changed, 604 insertions(+) > > > > > > diff --git > a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/DeferrableOutputStream.java > b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/DeferrableOutputStream.java > > > new file mode 100644 > > > index 00000000..6f9c57f1 > > > --- /dev/null > > > +++ > b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/DeferrableOutputStream.java > > > @@ -0,0 +1,369 @@ > > > +/* > > > + * Licensed to the Apache Software Foundation (ASF) under one or more > > > + * contributor license agreements. See the NOTICE file distributed > with > > > + * this work for additional information regarding copyright ownership. > > > + * The ASF licenses this file to You under the Apache License, > Version 2.0 > > > + * (the "License"); you may not use this file except in compliance > with > > > + * the License. You may obtain a copy of the License at > > > + * > > > + * http://www.apache.org/licenses/LICENSE-2.0 > > > + * > > > + * Unless required by applicable law or agreed to in writing, software > > > + * distributed under the License is distributed on an "AS IS" BASIS, > > > + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or > implied. > > > + * See the License for the specific language governing permissions and > > > + * limitations under the License. > > > + */ > > > +package org.apache.commons.fileupload2.core; > > > + > > > +import java.io.ByteArrayInputStream; > > > +import java.io.ByteArrayOutputStream; > > > +import java.io.IOException; > > > +import java.io.InputStream; > > > +import java.io.OutputStream; > > > +import java.nio.file.Files; > > > +import java.nio.file.Path; > > > +import java.util.function.Supplier; > > > + > > > + > > > +/** An {@link OutputStream}, which keeps its data in memory, until a > configured > > > + * threshold is reached. If that is the case, a temporary file is > being created, > > > + * and the in-memory data is transferred to that file. All following > data will > > > + * be written to that file, too. > > > + * > > > + * In other words: If an uploaded file is small, then it will be kept > completely > > > + * in memory. On the other hand, if the uploaded file's size exceeds > the > > > + * configured threshold, it it considered a large file, and the data > is kept > > > + * in a temporary file. > > > + * > > > + * More precisely, this output stream supports three modes of > operation: > > > + * <ol> > > > + * <li>{@code threshold=-1}: <em>Always</em> create a temporary > file, even if > > > + * the uploaded file is empty.</li> > > > + * <li>{@code threshold=0}: Don't create empty, temporary files. > (Create a > > > + * temporary file, as soon as the first byte is written.)</li> > > > + * <li>{@code threshold>0}: Create a temporary file, if the size > exceeds the > > > + * threshold, otherwise keep the file in memory.</li> > > > + * </ol> > > > + * > > > + * Technically, this is similar to > > > + * {@link org.apache.commons.io.output.DeferredFileOutputStream}, > which has > > > + * been used in the past, except that this implementation observes > > > + * a precisely specified behaviour, and semantics, that match the > needs of the > > > + * {@link DiskFileItem}. > > > + * > > > + * Background: Over the various versions of commons-io, the > > > + * {@link org.apache.commons.io.output.DeferredFileOutputStream} has > changed > > > + * semantics, and behaviour more than once. > > > + * (For details, see > > > + * <a href="https://issues.apache.org/jira/browse/FILEUPLOAD-295 > ">FILEUPLOAD-295</a>) > > > + */ > > > +public class DeferrableOutputStream extends OutputStream { > > > + /** This enumeration represents the possible states of the {@link > DeferrableOutputStream}. > > > + */ > > > + public enum State { > > > + /** The stream object has been created with a non-negative > threshold, > > > + * but so far no data has been written. > > > + */ > > > + initialized, > > > + /** The stream object has been created with a non-negative > threshold, > > > + * and some data has been written, but the threshold is not > yet exceeded, > > > + * and the data is still kept in memory. > > > + */ > > > + opened, > > > + /** Either of the following conditions is given: > > > + * <ol> > > > + * <li>The stream object has been created with a threshold > of -1, or</li> > > > + * <li>the stream object has been created with a > non-negative threshold, > > > + * and some data has been written. The number of bytes, > that have > > > + * been written, exceeds the configured threshold.</li> > > > + * </ol> > > > + * In either case, a temporary file has been created, and all > data has been > > > + * written to the temporary file, erasing all existing data > from memory. > > > + */ > > > + persisted, > > > + /** The stream has been closed, and data can no longer be > written. It is > > > + * now valid to invoke {@link > DeferrableOutputStream#getInputStream()}. > > > + */ > > > + closed > > > + } > > > + /** The configured threshold, as an integer. This variable isn't > actually > > > + * used. Instead {@link #longThreshold} is used. > > > + * @see #longThreshold > > > + */ > > > + private final int threshold; > > > + /** The configured threshold, as a long integer. (Using a long > integer > > > + * enables proper handling of the threshold, when the file size is > > > + * approaching {@link Integer#MAX_VALUE}. > > > + * @see #threshold > > > + */ > > > + private final long longThreshold; > > > + /** This supplier will be invoked, if the temporary file is > created, > > > + * to determine the temporary file's location. > > > + * @see #path > > > + */ > > > + private final Supplier<Path> pathSupplier; > > > + /** If a temporary file has been created: Path of the temporary > > > + * file. Otherwise null. > > > + * @see #pathSupplier > > > + */ > > > + private Path path; > > > + /** If no temporary file was created: A stream, to which the > > > + * incoming data is being written, until the threshold is reached. > > > + * Otherwise null. > > > + */ > > > + private ByteArrayOutputStream baos; > > > + /** If no temporary file was created, and the stream is closed: > > > + * The in-memory data, that was written to the stream. Otherwise > null. > > > + */ > > > + private byte[] bytes; > > > + /** If a temporary file has been created: An open stream > > > + * for writing to that file. Otherwise null. > > > + */ > > > + private OutputStream out; > > > + /** The streams current state. > > > + */ > > > + private State state; > > > + /** True, if the stream has ever been in state {@link > State#persisted}. > > > + * Or, in other words: True, if a temporary file has been created. > > > + */ > > > + private boolean wasPersisted; > > > + > > > + /** Creates a new instance with the given threshold, and the > given supplier for a > > > + * temporary files path. > > > + * If the threshold is -1, then the temporary file will be > created immediately, and > > > + * no in-memory data will be kept, at all. > > > + * If the threshold is 0, then the temporary file will be > created, as soon as the > > > + * first byte will be written, but no in-memory data will be kept. > > > + * If the threshold is >0, then the temporary file will be > created, as soon as that > > > + * number of bytes have been written. Up to that point, data will > be kept in an > > > + * in-memory buffer. > > > + * > > > + * @param threshold Either of -1 (Create the temporary file > immediately), 0 (Create > > > + * the temporary file, as soon as data is being written for the > first time), or >0 > > > + * (Keep data in memory, as long as the given number of bytes > is reached, then > > > + * create a temporary file, and continue using that). > > > + * @param pathSupplier A supplier for the temporary files path. > This supplier must > > > + * not return null. The file's directory will be created, if > necessary, by > > > + * invoking {@link Files#createDirectories(Path, > java.nio.file.attribute.FileAttribute...)}. > > > + * @throws IOException Creating the temporary file (in the case > of threshold -1) > > > + * has failed. > > > + */ > > > + public DeferrableOutputStream(final int threshold, final > Supplier<Path> pathSupplier) throws IOException { > > > + if (threshold < 0) { > > > + this.threshold = -1; > > > + } else { > > > + this.threshold = threshold; > > > + } > > > + longThreshold = (long) threshold; > > > + this.pathSupplier = pathSupplier; > > > + checkThreshold(0); > > > + } > > > + > > > + /** Returns the streams configured threshold. > > > + * @return The streams configured threshold. > > > + */ > > > + public int getThreshold() { > > > + return threshold; > > > + } > > > + > > > + /** Called to check, whether the threshold will be exceeded, if > the given number > > > + * of bytes are written to the stream. If so, persists the > in-memory data by > > > + * creating a new, temporary file, and writing the in-memory data > to the file. > > > + * @param numberOfIncomingBytes The number of bytes, which are > about to be written. > > > + * @return The actual output stream, to which the incoming data > may be written. > > > + * If the threshold is not yet exceeded, then this will be an > internal > > > + * {@link ByteArrayOutputStream}, otherwise a stream, which is > writing to the > > > + * temporary output file. > > > + * @throws IOException Persisting the in-memory data to a > temporary file > > > + * has failed. > > > + */ > > > + protected OutputStream checkThreshold(final int > numberOfIncomingBytes) throws IOException { > > > + if (state == null) { > > > + // Called from the constructor, state is unspecified. > > > + if (threshold == -1) { > > > + return persist(); > > > + } else { > > > + baos = new ByteArrayOutputStream(); > > > + bytes = null; > > > + state = State.initialized; > > > + return baos; > > > + } > > > + } else { > > > + switch (state) { > > > + case initialized: > > > + case opened: > > > + final int bytesWritten = baos.size(); > > > + if ((long) bytesWritten + (long) > numberOfIncomingBytes >= longThreshold) { > > > + return persist(); > > > + } > > > + if (numberOfIncomingBytes > 0) { > > > + state = State.opened; > > > + } > > > + return baos; > > > + case persisted: > > > + // Do nothing, we're staying in the current state. > > > + return out; > > > + case closed: > > > + // Do nothing, we're staying in the current state. > > > + return null; > > > + default: > > > + throw illegalStateError(); > > > + } > > > + } > > > + } > > > + > > > + /** Create the output file, change the state to {@code > persisted}, and > > > + * return an {@link OutputStream}, which is writing to that file. > > > + * @return The {@link OutputStream}, which is writing to the > created, > > > + * temporary file. > > > + * @throws IOException Creating the temporary file has failed. > > > + */ > > > + protected OutputStream persist() throws IOException { > > > + final Path p = pathSupplier.get(); > > > + final Path dir = p.getParent(); > > > + if (dir != null) { > > > + Files.createDirectories(dir); > > > + } > > > + final OutputStream os = Files.newOutputStream(p); > > > + if (baos != null) { > > > + baos.writeTo(os); > > > + } > > > + /** At this point, the output file has been successfully > created, > > > + * and we can safely switch state. > > > + */ > > > + state = State.persisted; > > > + wasPersisted = true; > > > + path = p; > > > + out = os; > > > + baos = null; > > > + bytes = null; > > > + return os; > > > + } > > > + > > > + @Override > > > + public void write(final int b) throws IOException { > > > + final OutputStream os = checkThreshold(1); > > > + if (os == null) { > > > + throw new IOException("This stream has already been > closed."); > > > + } > > > + bytes = null; > > > + os.write(b); > > > + } > > > + > > > + @Override > > > + public void write(final byte[] buffer) throws IOException { > > > + write(buffer, 0, buffer.length); > > > + } > > > + > > > + @Override > > > + public void write(final byte[] buffer, final int offset, final > int len) throws IOException { > > > + if (len > 0) { > > > + final OutputStream os = checkThreshold(len); > > > + if (os == null) { > > > + throw new IOException("This stream has already been > closed."); > > > + } > > > + bytes = null; > > > + os.write(buffer, offset, len); > > > + } > > > + } > > > + > > > + @Override > > > + public void close() throws IOException { > > > + switch (state) { > > > + case initialized: > > > + case opened: > > > + bytes = baos.toByteArray(); > > > + baos = null; > > > + state = State.closed; > > > + break; > > > + case persisted: > > > + bytes = null; > > > + out.close(); > > > + state = State.closed; > > > + break; > > > + case closed: > > > + // Already closed, do nothing. > > > + break; > > > + default: > > > + throw illegalStateError(); > > > + } > > > + } > > > + > > > + /** Returns true, if this stream was never persisted, > > > + * and no output file has been created. > > > + * @return True, if the stream was never in state > > > + * {@link State#persisted}, otherwise false. > > > + */ > > > + public boolean isInMemory() { > > > + switch (state) { > > > + case initialized: > > > + case opened: > > > + return true; > > > + case persisted: > > > + return false; > > > + case closed: > > > + return !wasPersisted; > > > + default: > > > + throw illegalStateError(); > > > + } > > > + } > > > + > > > + /** Returns the streams current state. > > > + * @return The streams current state. > > > + */ > > > + public State getState() { > > > + return state; > > > + } > > > + > > > + /** Returns the output file, that has been created, if any, or > null. > > > + * The latter is the case, if {@link #isInMemory()} returns true. > > > + * @return The output file, that has been created, if any, or > null. > > > + */ > > > + public Path getPath() { > > > + return path; > > > + } > > > + > > > + /** Returns the data, that has been written, if the stream has > > > + * been closed, and the stream is still in memory > > > + * ({@link #isInMemory()} returns true). Otherwise, returns null. > > > + * @return If the stream is closed (no more data can be written), > > > + * and the data is still in memory (no temporary file has been > > > + * created), returns the data, that has been written. Otherwise, > > > + * returns null. > > > + */ > > > + public byte[] getBytes() { > > > + return bytes; > > > + } > > > + > > > + /** If the stream is closed: Returns an {@link InputStream} on the > > > + * data, that has been written to this stream. Otherwise, throws > > > + * an {@link IllegalStateException}. > > > + * @return An {@link InputStream} on the data, that has been > > > + * written. Never null. > > > + * @throws IllegalStateException The stream has not yet been > > > + * closed. > > > + * @throws IOException Creating the {@link InputStream} has > > > + * failed. > > > + */ > > > + public InputStream getInputStream() throws IOException { > > > + if (state == State.closed) { > > > + if (bytes != null) { > > > + return new ByteArrayInputStream(bytes); > > > + } else { > > > + return Files.newInputStream(path); > > > + } > > > + } else { > > > + throw new IllegalStateException("This stream isn't yet > closed."); > > > + } > > > + } > > > + > > > + /** Returns the path of the output file, if such a file has > > > + * been created. That is the case, if {@link #isInMemory()} > > > + * returns false. Otherwise, returns null. > > > + * @return Path of the created output file, if any, or null. > > > + */ > > > + private IllegalStateException illegalStateError() { > > > + throw new IllegalStateException("Expected state > initialized|opened|persisted|closed, got " + state.name()); > > > + } > > > +} > > > diff --git > a/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/DeferrableOutputStreamTest.java > b/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/DeferrableOutputStreamTest.java > > > new file mode 100644 > > > index 00000000..60a0e83e > > > --- /dev/null > > > +++ > b/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/DeferrableOutputStreamTest.java > > > @@ -0,0 +1,235 @@ > > > +/* > > > + * Licensed to the Apache Software Foundation (ASF) under one or more > > > + * contributor license agreements. See the NOTICE file distributed > with > > > + * this work for additional information regarding copyright ownership. > > > + * The ASF licenses this file to You under the Apache License, > Version 2.0 > > > + * (the "License"); you may not use this file except in compliance > with > > > + * the License. You may obtain a copy of the License at > > > + * > > > + * http://www.apache.org/licenses/LICENSE-2.0 > > > + * > > > + * Unless required by applicable law or agreed to in writing, software > > > + * distributed under the License is distributed on an "AS IS" BASIS, > > > + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or > implied. > > > + * See the License for the specific language governing permissions and > > > + * limitations under the License. > > > + */ > > > +package org.apache.commons.fileupload2.core; > > > + > > > +import static org.junit.jupiter.api.Assertions.*; > > > + > > > +import java.io.ByteArrayOutputStream; > > > +import java.io.IOException; > > > +import java.io.InputStream; > > > +import java.io.OutputStream; > > > +import java.io.UncheckedIOException; > > > +import java.nio.charset.StandardCharsets; > > > +import java.nio.file.Files; > > > +import java.nio.file.Path; > > > +import java.nio.file.Paths; > > > +import java.util.function.Consumer; > > > +import java.util.function.Supplier; > > > + > > > +import org.junit.jupiter.api.BeforeAll; > > > +import org.junit.jupiter.api.Test; > > > + > > > +import > org.apache.commons.fileupload2.core.DeferrableOutputStream.State; > > > + > > > + > > > +/** Test suite for the {@link DeferrableOutputStream}. > > > + */ > > > +class DeferrableOutputStreamTest { > > > + private static final Path testDir = > Paths.get("target/unit-tests/DeferrableOutputStreamTest"); > > > + private static Path tempTestDir; > > > + private Supplier<Path> testFileSupplier = () -> { > > > + try { > > > + return Files.createTempFile(tempTestDir, > "testFile", ".bin"); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + }; > > > + > > > + @BeforeAll > > > + static void setUpTestDirs() throws IOException { > > > + Files.createDirectories(testDir); > > > + tempTestDir = Files.createTempDirectory(testDir, > "testDir"); > > > + } > > > + > > > + /** Tests using the {@link DeferrableOutputStream} with a > positive threshold. > > > + */ > > > + @Test > > > + void testExceedPositiveThreshold() { > > > + DeferrableOutputStream[] streams = new > DeferrableOutputStream[1]; > > > + final Consumer<Consumer<OutputStream>> tester = > (consumer) -> { > > > + try (final DeferrableOutputStream dos = new > DeferrableOutputStream(5, testFileSupplier)) { > > > + streams[0] = dos; > > > + assertTrue(dos.isInMemory()); > > > + assertNull(dos.getPath()); > > > + assertNull(dos.getBytes()); > > > + assertSame(State.initialized, > dos.getState()); > > > + for (int i = 0; i < 4; i++) { > > > + try { > > > + dos.write('.'); > > > + } catch (IOException ioe) { > > > + throw new > UncheckedIOException(ioe); > > > + } > > > + assertSame(State.opened, > dos.getState()); > > > + assertTrue(dos.isInMemory()); > > > + assertNull(dos.getPath()); > > > + assertNull(dos.getBytes()); > > > + } > > > + consumer.accept(dos); > > > + assertFalse(dos.isInMemory()); > > > + assertNotNull(dos.getPath()); > > > + assertNull(dos.getBytes()); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + > > > + final DeferrableOutputStream dos = streams[0]; > > > + assertFalse(dos.isInMemory()); > > > + assertNotNull(dos.getPath()); > > > + assertTrue(Files.isRegularFile(dos.getPath())); > > > + final byte[] actual; > > > + try (InputStream is = dos.getInputStream()) { > > > + actual = read(is); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + final byte[] expect = > "....,".getBytes(StandardCharsets.UTF_8); > > > + assertArrayEquals(expect, actual); > > > + }; > > > + > > > + // Break the threshold using OutputStream.write(int); > > > + tester.accept((os) -> { > > > + try { > > > + os.write(','); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + }); > > > + // Break the threshold using > OutputStream.write(byte[]); > > > + tester.accept((os) -> { > > > + final byte[] buffer = new byte[] {','}; > > > + try { > > > + os.write(buffer); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + }); > > > + // Break the threshold using > OutputStream.write(byte[], int, int); > > > + tester.accept((os) -> { > > > + final byte[] buffer = new byte[] {',', '-'}; > > > + try { > > > + os.write(buffer, 0, 1); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + }); > > > + } > > > + > > > + /** Tests using the {@link DeferrableOutputStream} with > threshold 0. > > > + */ > > > + @Test > > > + void testThresholdZero() { > > > + DeferrableOutputStream[] streams = new > DeferrableOutputStream[1]; > > > + final Consumer<Consumer<OutputStream>> tester = > (consumer) -> { > > > + try (final DeferrableOutputStream dos = new > DeferrableOutputStream(0, testFileSupplier)) { > > > + streams[0] = dos; > > > + assertTrue(dos.isInMemory()); > > > + assertNull(dos.getPath()); > > > + assertNull(dos.getBytes()); > > > + assertSame(State.initialized, > dos.getState()); > > > + consumer.accept(dos); > > > + assertFalse(dos.isInMemory()); > > > + assertNotNull(dos.getPath()); > > > + assertNull(dos.getBytes()); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + > > > + final DeferrableOutputStream dos = streams[0]; > > > + assertFalse(dos.isInMemory()); > > > + assertNotNull(dos.getPath()); > > > + assertTrue(Files.isRegularFile(dos.getPath())); > > > + final byte[] actual; > > > + try (InputStream is = dos.getInputStream()) { > > > + actual = read(is); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + final byte[] expect = > ",".getBytes(StandardCharsets.UTF_8); > > > + assertArrayEquals(expect, actual); > > > + }; > > > + // Break the threshold using OutputStream.write(int); > > > + tester.accept((os) -> { > > > + try { > > > + os.write(','); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + }); > > > + // Break the threshold using > OutputStream.write(byte[]); > > > + tester.accept((os) -> { > > > + final byte[] buffer = new byte[] {','}; > > > + try { > > > + os.write(buffer); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + }); > > > + // Break the threshold using > OutputStream.write(byte[], int, int); > > > + tester.accept((os) -> { > > > + final byte[] buffer = new byte[] {',', '-'}; > > > + try { > > > + os.write(buffer, 0, 1); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + }); > > > + } > > > + > > > + /** Tests using the {@link DeferrableOutputStream} with > threshold -1. > > > + */ > > > + @Test > > > + void testThresholdMinusOne() { > > > + DeferrableOutputStream[] streams = new > DeferrableOutputStream[1]; > > > + final Runnable tester = () -> { > > > + try (final DeferrableOutputStream dos = new > DeferrableOutputStream(-1, testFileSupplier)) { > > > + streams[0] = dos; > > > + assertFalse(dos.isInMemory()); > > > + assertNotNull(dos.getPath()); > > > + assertNull(dos.getBytes()); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + > > > + final DeferrableOutputStream dos = streams[0]; > > > + assertFalse(dos.isInMemory()); > > > + assertNotNull(dos.getPath()); > > > + assertTrue(Files.isRegularFile(dos.getPath())); > > > + final byte[] actual; > > > + try (InputStream is = dos.getInputStream()) { > > > + actual = read(is); > > > + } catch (IOException ioe) { > > > + throw new UncheckedIOException(ioe); > > > + } > > > + final byte[] expect = > "".getBytes(StandardCharsets.UTF_8); > > > + assertArrayEquals(expect, actual); > > > + }; > > > + tester.run(); > > > + } > > > + > > > + protected byte[] read(InputStream pIs) throws IOException { > > > + final ByteArrayOutputStream baos = new > ByteArrayOutputStream(); > > > + final byte[] buffer = new byte[8192]; > > > + for (;;) { > > > + final int res = pIs.read(buffer); > > > + if (res == -1) { > > > + return baos.toByteArray(); > > > + } else if (res > 0) { > > > + baos.write(buffer, 0, res); > > > + } > > > + } > > > + } > > > +} > > > > > > > --------------------------------------------------------------------- > > To unsubscribe, e-mail: dev-unsubscr...@commons.apache.org > > For additional commands, e-mail: dev-h...@commons.apache.org > > > > > -- > The woman was born in a full-blown thunderstorm. She probably told it > to be quiet. It probably did. (Robert Jordan, Winter's heart) > > --------------------------------------------------------------------- > To unsubscribe, e-mail: dev-unsubscr...@commons.apache.org > For additional commands, e-mail: dev-h...@commons.apache.org > >