This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-io.git
commit 8acfd4c594eb9838d7de38eeb097da050c149a80 Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Sat May 20 12:08:29 2023 -0400 Add ChunkedOutputStream.Builder and refactor - Javadoc - In builders, a negative or zero size buffer should reset the buffer size to it default buffer size value - In builders, AbstractOriginSupplier.checkOrigin() now throws IllegalStateException instead of NullPointerException --- src/changes/changes.xml | 6 ++ .../commons/io/build/AbstractOriginSupplier.java | 7 +- .../commons/io/build/AbstractStreamBuilder.java | 34 +++++++-- .../io/input/BufferedFileChannelInputStream.java | 2 +- .../io/input/MemoryMappedFileInputStream.java | 2 +- .../apache/commons/io/input/ReaderInputStream.java | 3 +- .../commons/io/input/ReversedLinesFileReader.java | 2 +- .../commons/io/input/UncheckedBufferedReader.java | 3 +- .../commons/io/input/UncheckedFilterReader.java | 3 +- .../input/UnsynchronizedByteArrayInputStream.java | 3 +- .../commons/io/output/ChunkedOutputStream.java | 87 +++++++++++++++++++--- .../commons/io/output/FileWriterWithEncoding.java | 3 +- .../commons/io/output/LockableFileWriter.java | 3 +- .../commons/io/output/ChunkedOutputStreamTest.java | 52 +++++++++---- .../commons/io/output/LockableFileWriterTest.java | 2 +- 15 files changed, 167 insertions(+), 45 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index c2a34531..b364456c 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -55,10 +55,16 @@ The <action> type attribute can be add,update,fix,remove. <action dev="ggregory" type="fix" due-to="Gary Gregory"> ByteArrayOrigin should be able convert a byte[] to a ByteArrayInputStream. </action> + <action dev="ggregory" type="fix" due-to="Gary Gregory"> + AbstractOriginSupplier.checkOrigin() now throws IllegalStateException instead of NullPointerException. + </action> <!-- ADD --> <action dev="ggregory" type="add" due-to="Gary Gregory"> Add CharSequenceInputStream.Builder. </action> + <action dev="ggregory" type="add" due-to="Gary Gregory"> + Add ChunkedOutputStream.Builder. + </action> <action dev="ggregory" type="add" due-to="Gary Gregory"> Add AbstractStreamBuilder.setOpenOptions(OpenOption...). </action> diff --git a/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java b/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java index ef691f44..ae8472c2 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java +++ b/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java @@ -166,10 +166,13 @@ public abstract class AbstractOriginSupplier<T, B extends AbstractOriginSupplier * Checks whether the origin is null. * * @return the origin. - * @throws NullPointerException if the {@code origin} is {@code null} + * @throws IllegalStateException if the {@code origin} is {@code null}. */ protected AbstractOrigin<?, ?> checkOrigin() { - return Objects.requireNonNull(origin, "origin"); + if (origin == null) { + throw new IllegalStateException("origin == null"); + } + return origin; } /** diff --git a/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java b/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java index 3cbe1698..f38bed07 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java +++ b/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java @@ -23,6 +23,7 @@ import java.io.OutputStream; import java.io.Writer; import java.nio.charset.Charset; import java.nio.file.OpenOption; +import java.nio.file.Path; import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; @@ -85,11 +86,12 @@ public abstract class AbstractStreamBuilder<T, B extends AbstractStreamBuilder<T * @return An input stream * @throws IOException if an I/O error occurs. * @throws UnsupportedOperationException if the origin cannot be converted to a CharSequence. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getCharSequence(Charset) * @since 2.13.0 */ protected CharSequence getCharSequence() throws IOException { - return getOrigin().getCharSequence(getCharset()); + return checkOrigin().getCharSequence(getCharset()); } /** @@ -117,10 +119,11 @@ public abstract class AbstractStreamBuilder<T, B extends AbstractStreamBuilder<T * @throws IOException if an I/O error occurs. * @throws UnsupportedOperationException if the origin cannot be converted to an InputStream. * @see AbstractOrigin#getInputStream(OpenOption...) + * @throws IllegalStateException if the {@code origin} is {@code null}. * @since 2.13.0 */ protected InputStream getInputStream() throws IOException { - return getOrigin().getInputStream(getOpenOptions()); + return checkOrigin().getInputStream(getOpenOptions()); } protected OpenOption[] getOpenOptions() { @@ -128,16 +131,30 @@ public abstract class AbstractStreamBuilder<T, B extends AbstractStreamBuilder<T } /** - * Gets an output stream from the origin with open options. + * Gets an OutputStream from the origin with open options. * - * @return An input stream + * @return An OutputStream * @throws IOException if an I/O error occurs. * @throws UnsupportedOperationException if the origin cannot be converted to an OututStream. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getOutputStream(OpenOption...) * @since 2.13.0 */ protected OutputStream getOutputStream() throws IOException { - return getOrigin().getOutputStream(getOpenOptions()); + return checkOrigin().getOutputStream(getOpenOptions()); + } + + /** + * Gets a Path from the origin. + * + * @return A Path + * @throws UnsupportedOperationException if the origin cannot be converted to a Path. + * @throws IllegalStateException if the {@code origin} is {@code null}. + * @see AbstractOrigin#getPath() + * @since 2.13.0 + */ + protected Path getPath() { + return checkOrigin().getPath(); } /** @@ -146,15 +163,16 @@ public abstract class AbstractStreamBuilder<T, B extends AbstractStreamBuilder<T * @return An writer. * @throws IOException if an I/O error occurs. * @throws UnsupportedOperationException if the origin cannot be converted to a Writer. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getOutputStream(OpenOption...) * @since 2.13.0 */ protected Writer getWriter() throws IOException { - return getOrigin().getWriter(getCharset(), getOpenOptions()); + return checkOrigin().getWriter(getCharset(), getOpenOptions()); } /** - * Sets the buffer size. + * Sets the buffer size. Invalid input (bufferSize <= 0) resets the value to its default. * <p> * Subclasses may ignore this setting. * </p> @@ -163,7 +181,7 @@ public abstract class AbstractStreamBuilder<T, B extends AbstractStreamBuilder<T * @return this. */ public B setBufferSize(final int bufferSize) { - this.bufferSize = bufferSize >= 0 ? bufferSize : bufferSizeDefault; + this.bufferSize = bufferSize > 0 ? bufferSize : bufferSizeDefault; return asThis(); } diff --git a/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java b/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java index ea75cc7d..e0d6734f 100644 --- a/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java +++ b/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java @@ -86,7 +86,7 @@ public final class BufferedFileChannelInputStream extends InputStream { */ @Override public BufferedFileChannelInputStream get() throws IOException { - return new BufferedFileChannelInputStream(getOrigin().getPath(), getBufferSize()); + return new BufferedFileChannelInputStream(getPath(), getBufferSize()); } } diff --git a/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java b/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java index 1935fa99..89114be9 100644 --- a/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java +++ b/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java @@ -108,7 +108,7 @@ public final class MemoryMappedFileInputStream extends InputStream { */ @Override public MemoryMappedFileInputStream get() throws IOException { - return new MemoryMappedFileInputStream(getOrigin().getPath(), getBufferSize()); + return new MemoryMappedFileInputStream(getPath(), getBufferSize()); } } diff --git a/src/main/java/org/apache/commons/io/input/ReaderInputStream.java b/src/main/java/org/apache/commons/io/input/ReaderInputStream.java index 27ea5ab8..7e5bee6f 100644 --- a/src/main/java/org/apache/commons/io/input/ReaderInputStream.java +++ b/src/main/java/org/apache/commons/io/input/ReaderInputStream.java @@ -114,12 +114,13 @@ public class ReaderInputStream extends InputStream { * * @return a new instance. * @throws UnsupportedOperationException if the origin cannot provide a Reader. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getReader(Charset) */ @SuppressWarnings("resource") @Override public ReaderInputStream get() throws IOException { - return new ReaderInputStream(getOrigin().getReader(getCharset()), charsetEncoder, getBufferSize()); + return new ReaderInputStream(checkOrigin().getReader(getCharset()), charsetEncoder, getBufferSize()); } @Override diff --git a/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java b/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java index 75052f55..ef5fdf13 100644 --- a/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java +++ b/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java @@ -87,7 +87,7 @@ public class ReversedLinesFileReader implements Closeable { */ @Override public ReversedLinesFileReader get() throws IOException { - return new ReversedLinesFileReader(getOrigin().getPath(), getBufferSize(), getCharset()); + return new ReversedLinesFileReader(getPath(), getBufferSize(), getCharset()); } } diff --git a/src/main/java/org/apache/commons/io/input/UncheckedBufferedReader.java b/src/main/java/org/apache/commons/io/input/UncheckedBufferedReader.java index 7c7e9482..1a49c9bb 100644 --- a/src/main/java/org/apache/commons/io/input/UncheckedBufferedReader.java +++ b/src/main/java/org/apache/commons/io/input/UncheckedBufferedReader.java @@ -78,12 +78,13 @@ public final class UncheckedBufferedReader extends BufferedReader { * * @return a new instance. * @throws UnsupportedOperationException if the origin cannot provide a Reader. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getReader(Charset) */ @Override public UncheckedBufferedReader get() { // This an unchecked class, so this method is as well. - return Uncheck.get(() -> new UncheckedBufferedReader(getOrigin().getReader(getCharset()), getBufferSize())); + return Uncheck.get(() -> new UncheckedBufferedReader(checkOrigin().getReader(getCharset()), getBufferSize())); } } diff --git a/src/main/java/org/apache/commons/io/input/UncheckedFilterReader.java b/src/main/java/org/apache/commons/io/input/UncheckedFilterReader.java index 4137a562..768a16b7 100644 --- a/src/main/java/org/apache/commons/io/input/UncheckedFilterReader.java +++ b/src/main/java/org/apache/commons/io/input/UncheckedFilterReader.java @@ -74,12 +74,13 @@ public final class UncheckedFilterReader extends FilterReader { * * @return a new instance. * @throws UnsupportedOperationException if the origin cannot provide a Reader. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getReader(Charset) */ @Override public UncheckedFilterReader get() { // This an unchecked class, so this method is as well. - return Uncheck.get(() -> new UncheckedFilterReader(getOrigin().getReader(getCharset()))); + return Uncheck.get(() -> new UncheckedFilterReader(checkOrigin().getReader(getCharset()))); } } diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java index 38e9a2d9..aecf72e3 100644 --- a/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java +++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java @@ -92,11 +92,12 @@ public class UnsynchronizedByteArrayInputStream extends InputStream { * * @return a new instance. * @throws UnsupportedOperationException if the origin cannot provide a byte[]. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getByteArray() */ @Override public UnsynchronizedByteArrayInputStream get() throws IOException { - return new UnsynchronizedByteArrayInputStream(getOrigin().getByteArray(), offset, length); + return new UnsynchronizedByteArrayInputStream(checkOrigin().getByteArray(), offset, length); } @Override diff --git a/src/main/java/org/apache/commons/io/output/ChunkedOutputStream.java b/src/main/java/org/apache/commons/io/output/ChunkedOutputStream.java index 7d193cb0..581ae126 100644 --- a/src/main/java/org/apache/commons/io/output/ChunkedOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/ChunkedOutputStream.java @@ -21,16 +21,73 @@ import java.io.IOException; import java.io.OutputStream; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.build.AbstractStreamBuilder; /** - * OutputStream which breaks larger output blocks into chunks. - * Native code may need to copy the input array; if the write buffer - * is very large this can cause OOME. + * OutputStream which breaks larger output blocks into chunks. Native code may need to copy the input array; if the write buffer is very large this can cause + * OOME. * * @since 2.5 */ public class ChunkedOutputStream extends FilterOutputStream { + // @formatter:off + /** + * Builds a new {@link UnsynchronizedByteArrayOutputStream} instance. + * <p> + * Using File IO: + * </p> + * + * <pre>{@code + * UnsynchronizedByteArrayOutputStream s = UnsynchronizedByteArrayOutputStream.builder() + * .setBufferSize(8192) + * .get(); + * } + * </pre> + * <p> + * Using NIO Path: + * </p> + * + * <pre>{@code + * UnsynchronizedByteArrayOutputStream s = UnsynchronizedByteArrayOutputStream.builder() + * .setBufferSize(8192) + * .get(); + * } + * </pre> + * + * @since 2.13.0 + */ + // @formatter:on + public static class Builder extends AbstractStreamBuilder<ChunkedOutputStream, Builder> { + + /** + * Constructs a new instance. + * <p> + * This builder use the aspects OutputStream and buffer size (chunk size). + * </p> + * + * @return a new instance. + * @throws IOException if an I/O error occurs. + * @throws UnsupportedOperationException if the origin cannot be converted to an OututStream. + * @see #getOutputStream() + */ + @Override + public ChunkedOutputStream get() throws IOException { + return new ChunkedOutputStream(getOutputStream(), getBufferSize()); + } + + } + + /** + * Constructs a new {@link Builder}. + * + * @return a new {@link Builder}. + * @since 2.13.0 + */ + public static Builder builder() { + return new Builder(); + } + /** * The maximum chunk size to us when writing data arrays */ @@ -40,7 +97,9 @@ public class ChunkedOutputStream extends FilterOutputStream { * Creates a new stream that uses a chunk size of {@link IOUtils#DEFAULT_BUFFER_SIZE}. * * @param stream the stream to wrap + * @deprecated Use {@link #builder()} and {@link Builder#get()} */ + @Deprecated public ChunkedOutputStream(final OutputStream stream) { this(stream, IOUtils.DEFAULT_BUFFER_SIZE); } @@ -48,24 +107,30 @@ public class ChunkedOutputStream extends FilterOutputStream { /** * Creates a new stream that uses the specified chunk size. * - * @param stream the stream to wrap + * @param stream the stream to wrap * @param chunkSize the chunk size to use; must be a positive number. * @throws IllegalArgumentException if the chunk size is <= 0 + * @deprecated Use {@link #builder()} and {@link Builder#get()} */ + @Deprecated public ChunkedOutputStream(final OutputStream stream, final int chunkSize) { - super(stream); - if (chunkSize <= 0) { - throw new IllegalArgumentException(); - } - this.chunkSize = chunkSize; + super(stream); + if (chunkSize <= 0) { + throw new IllegalArgumentException("chunkSize <= 0"); + } + this.chunkSize = chunkSize; + } + + int getChunkSize() { + return chunkSize; } /** * Writes the data buffer in chunks to the underlying stream * - * @param data the data to write + * @param data the data to write * @param srcOffset the offset - * @param length the length of data to write + * @param length the length of data to write * * @throws IOException if an I/O error occurs. */ diff --git a/src/main/java/org/apache/commons/io/output/FileWriterWithEncoding.java b/src/main/java/org/apache/commons/io/output/FileWriterWithEncoding.java index 27bdb603..45cb6dec 100644 --- a/src/main/java/org/apache/commons/io/output/FileWriterWithEncoding.java +++ b/src/main/java/org/apache/commons/io/output/FileWriterWithEncoding.java @@ -94,6 +94,7 @@ public class FileWriterWithEncoding extends ProxyWriter { * * @return a new instance. * @throws UnsupportedOperationException if the origin cannot provide a File. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getFile() */ @SuppressWarnings("resource") @@ -103,7 +104,7 @@ public class FileWriterWithEncoding extends ProxyWriter { throw new IllegalStateException(String.format("Mismatched Charset(%s) and CharsetEncoder(%s)", getCharset(), charsetEncoder.charset())); } final Object encoder = charsetEncoder != null ? charsetEncoder : getCharset(); - return new FileWriterWithEncoding(FileWriterWithEncoding.initWriter(getOrigin().getFile(), encoder, append)); + return new FileWriterWithEncoding(FileWriterWithEncoding.initWriter(checkOrigin().getFile(), encoder, append)); } /** diff --git a/src/main/java/org/apache/commons/io/output/LockableFileWriter.java b/src/main/java/org/apache/commons/io/output/LockableFileWriter.java index 052fc4ca..dd730e73 100644 --- a/src/main/java/org/apache/commons/io/output/LockableFileWriter.java +++ b/src/main/java/org/apache/commons/io/output/LockableFileWriter.java @@ -87,11 +87,12 @@ public class LockableFileWriter extends Writer { * * @return a new instance. * @throws UnsupportedOperationException if the origin cannot provide a File. + * @throws IllegalStateException if the {@code origin} is {@code null}. * @see AbstractOrigin#getFile() */ @Override public LockableFileWriter get() throws IOException { - return new LockableFileWriter(getOrigin().getFile(), getCharset(), append, lockDirectory.getFile().toString()); + return new LockableFileWriter(checkOrigin().getFile(), getCharset(), append, lockDirectory.getFile().toString()); } /** diff --git a/src/test/java/org/apache/commons/io/output/ChunkedOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/ChunkedOutputStreamTest.java index 54287add..b7460a4d 100644 --- a/src/test/java/org/apache/commons/io/output/ChunkedOutputStreamTest.java +++ b/src/test/java/org/apache/commons/io/output/ChunkedOutputStreamTest.java @@ -30,39 +30,63 @@ import org.junit.jupiter.api.Test; */ public class ChunkedOutputStreamTest { + private ByteArrayOutputStream newByteArrayOutputStream(final AtomicInteger numWrites) { + return new ByteArrayOutputStream() { + @Override + public void write(final byte[] b, final int off, final int len) { + numWrites.incrementAndGet(); + super.write(b, off, len); + } + }; + } + @Test - public void defaultConstructor() throws IOException { + public void testDefaultBuilder() throws IOException { final AtomicInteger numWrites = new AtomicInteger(); try (ByteArrayOutputStream baos = newByteArrayOutputStream(numWrites); - final ChunkedOutputStream chunked = new ChunkedOutputStream(baos)) { + final ChunkedOutputStream chunked = ChunkedOutputStream.builder().setOutputStream(baos).get()) { chunked.write(new byte[IOUtils.DEFAULT_BUFFER_SIZE + 1]); assertEquals(2, numWrites.get()); } + assertThrows(IllegalStateException.class, () -> ChunkedOutputStream.builder().get()); } @Test - public void negative_chunkSize_not_permitted() { - assertThrows(IllegalArgumentException.class, () -> new ChunkedOutputStream(new ByteArrayOutputStream(), 0)); + public void testDefaultConstructor() throws IOException { + final AtomicInteger numWrites = new AtomicInteger(); + try (ByteArrayOutputStream baos = newByteArrayOutputStream(numWrites); + final ChunkedOutputStream chunked = new ChunkedOutputStream(baos)) { + chunked.write(new byte[IOUtils.DEFAULT_BUFFER_SIZE + 1]); + assertEquals(2, numWrites.get()); + } } - private ByteArrayOutputStream newByteArrayOutputStream(final AtomicInteger numWrites) { - return new ByteArrayOutputStream() { - @Override - public void write(final byte[] b, final int off, final int len) { - numWrites.incrementAndGet(); - super.write(b, off, len); - } - }; + @Test + public void testNegativeChunkSize() throws IOException { + assertThrows(IllegalArgumentException.class, () -> new ChunkedOutputStream(new ByteArrayOutputStream(), -1)); + // Builder resets invalid input to the default. + try (ChunkedOutputStream os = ChunkedOutputStream.builder().setOutputStream(new ByteArrayOutputStream()).setBufferSize(-1).get()) { + assertEquals(IOUtils.DEFAULT_BUFFER_SIZE, os.getChunkSize()); + } } @Test - public void write_four_chunks() throws Exception { + public void testWriteFourChunks() throws Exception { final AtomicInteger numWrites = new AtomicInteger(); try (ByteArrayOutputStream baos = newByteArrayOutputStream(numWrites); - final ChunkedOutputStream chunked = new ChunkedOutputStream(baos, 10)) { + final ChunkedOutputStream chunked = new ChunkedOutputStream(baos, 10)) { chunked.write("0123456789012345678901234567891".getBytes()); assertEquals(4, numWrites.get()); } } + @Test + public void testZeroChunkSize() throws IOException { + assertThrows(IllegalArgumentException.class, () -> new ChunkedOutputStream(new ByteArrayOutputStream(), 0)); + // Builder resets invalid input to the default. + try (ChunkedOutputStream os = ChunkedOutputStream.builder().setOutputStream(new ByteArrayOutputStream()).setBufferSize(0).get()) { + assertEquals(IOUtils.DEFAULT_BUFFER_SIZE, os.getChunkSize()); + } + } + } diff --git a/src/test/java/org/apache/commons/io/output/LockableFileWriterTest.java b/src/test/java/org/apache/commons/io/output/LockableFileWriterTest.java index 5afef834..88c117da 100644 --- a/src/test/java/org/apache/commons/io/output/LockableFileWriterTest.java +++ b/src/test/java/org/apache/commons/io/output/LockableFileWriterTest.java @@ -134,7 +134,7 @@ public class LockableFileWriterTest { assertFalse(file.exists()); assertFalse(lockFile.exists()); // - assertThrows(NullPointerException.class, () -> LockableFileWriter.builder().get()); + assertThrows(IllegalStateException.class, () -> LockableFileWriter.builder().get()); assertFalse(file.exists()); assertFalse(lockFile.exists()); // again