This is an automated email from the ASF dual-hosted git repository. pkarwasz pushed a commit to branch feat/channel-to-byte-array in repository https://gitbox.apache.org/repos/asf/commons-io.git
commit 6f9d2b8902d3370a6d406e3584eaf505d04e7185 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Sun Sep 21 18:03:29 2025 +0200 feat: add NIO channel support to `AbstractStreamBuilder` Some builders derived from `AbstractStreamBuilder` (for example, `SevenZFile.Builder` in Commons Compress) need to produce a `SeekableByteChannel` as their data source. Until now this required ad-hoc `instanceof` switches across different origin types. This change integrates channel support directly into the origin/builder abstraction, leading to a cleaner and more object-oriented design. ### Key changes * Add `getReadableByteChannel()` and `getWritableByteChannel()` to `AbstractOrigin` and propagate to `AbstractStreamBuilder`. * Introduce `ChannelOrigin`, an `AbstractOrigin` implementation backed by an existing `ReadableByteChannel`/`WritableByteChannel`. * Add `ByteArrayChannel`, a simple in-memory `SeekableByteChannel` implementation. * Extend unit tests to cover the new methods and types. --- src/changes/changes.xml | 2 + src/main/java/org/apache/commons/io/IOUtils.java | 5 +- .../apache/commons/io/build/AbstractOrigin.java | 472 +++++++++++++++++++-- .../commons/io/build/AbstractOriginSupplier.java | 30 ++ .../commons/io/build/AbstractStreamBuilder.java | 32 ++ .../commons/io/channels/ByteArrayChannel.java | 241 +++++++++++ .../commons/io/build/AbstractOriginTest.java | 125 +++++- .../build/AbstractRandomAccessFileOriginTest.java | 11 + .../io/build/AbstractStreamBuilderTest.java | 66 +++ .../commons/io/build/ByteArrayOriginTest.java | 20 +- .../apache/commons/io/build/ChannelOriginTest.java | 108 +++++ .../commons/io/build/CharSequenceOriginTest.java | 35 +- .../apache/commons/io/build/FileOriginTest.java | 15 +- .../io/build/IORandomAccessFileOriginTest.java | 6 +- .../commons/io/build/InputStreamOriginTest.java | 21 +- .../commons/io/build/OutputStreamOriginTest.java | 27 +- .../apache/commons/io/build/PathOriginTest.java | 14 +- .../io/build/RandomAccessFileOriginTest.java | 5 +- .../apache/commons/io/build/ReaderOriginTest.java | 21 +- .../org/apache/commons/io/build/URIOriginTest.java | 14 +- .../commons/io/build/WriterStreamOriginTest.java | 26 +- .../commons/io/channels/ByteArrayChannelTest.java | 241 +++++++++++ 22 files changed, 1439 insertions(+), 98 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 0b72c76f9..e6a84b477 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -62,6 +62,8 @@ The <action> type attribute can be add,update,fix,remove. <action dev="ggregory" type="add" due-to="Gary Gregory">Add org.apache.commons.io.output.ProxyOutputStream.writeRepeat(int, long).</action> <action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add length unit support in FileSystem limits.</action> <action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add IOUtils.toByteArray(InputStream, int, int) for safer chunked reading with size validation.</action> + <action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add ByteArrayChannel, a simple in-memory `SeekableByteChannel` implementation.</action> + <action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add NIO channel support to `AbstractStreamBuilder`.</action> <!-- UPDATE --> <action type="update" dev="ggregory" due-to="Gary Gregory, Dependabot">Bump org.apache.commons:commons-parent from 85 to 87 #774.</action> <action type="update" dev="ggregory" due-to="Gary Gregory">[test] Bump commons-codec:commons-codec from 1.18.0 to 1.19.0.</action> diff --git a/src/main/java/org/apache/commons/io/IOUtils.java b/src/main/java/org/apache/commons/io/IOUtils.java index cff8e4c23..6f41e8330 100644 --- a/src/main/java/org/apache/commons/io/IOUtils.java +++ b/src/main/java/org/apache/commons/io/IOUtils.java @@ -224,10 +224,11 @@ public class IOUtils { /** * The maximum size of an array in many Java VMs. * <p> - * The constant is copied from OpenJDK's {@link jdk.internal.util.ArraysSupport#SOFT_MAX_ARRAY_LENGTH}. + * The constant is copied from OpenJDK's {@code jdk.internal.util.ArraysSupport#SOFT_MAX_ARRAY_LENGTH}. * </p> + * @since 2.21.0 */ - private static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8; + public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8; /** * Returns the given InputStream if it is already a {@link BufferedInputStream}, otherwise creates a diff --git a/src/main/java/org/apache/commons/io/build/AbstractOrigin.java b/src/main/java/org/apache/commons/io/build/AbstractOrigin.java index 3c81bfd38..8de5ea378 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractOrigin.java +++ b/src/main/java/org/apache/commons/io/build/AbstractOrigin.java @@ -14,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.commons.io.build; import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -29,13 +29,17 @@ import java.io.Reader; import java.io.Writer; import java.net.URI; +import java.nio.channels.Channel; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.nio.file.spi.FileSystemProvider; import java.util.Arrays; import java.util.Objects; @@ -44,7 +48,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.io.RandomAccessFileMode; import org.apache.commons.io.RandomAccessFiles; -import org.apache.commons.io.file.spi.FileSystemProviders; +import org.apache.commons.io.channels.ByteArrayChannel; import org.apache.commons.io.input.BufferedFileChannelInputStream; import org.apache.commons.io.input.CharSequenceInputStream; import org.apache.commons.io.input.CharSequenceReader; @@ -53,15 +57,246 @@ import org.apache.commons.io.output.WriterOutputStream; /** - * Abstracts the origin of data for builders like a {@link File}, {@link Path}, {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, and - * {@link URI}. + * Abstract base class that encapsulates the <em>origin</em> of data used by Commons IO builders. + * <p> + * An origin represents where bytes/characters come from or go to, such as a {@link File}, {@link Path}, + * {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, or {@link URI}. Concrete subclasses + * expose only the operations that make sense for the underlying source or sink; invoking an unsupported operation + * results in {@link UnsupportedOperationException} (see, for example, {@link #getFile()} and {@link #getPath()}). + * </p> + * * <p> - * Some methods may throw {@link UnsupportedOperationException} if that method is not implemented in a concrete subclass, see {@link #getFile()} and - * {@link #getPath()}. + * The table below summarizes which views and conversions are supported for each origin type. + * Column headers show the target view; cells indicate whether that view is available from the origin in that row. * </p> * - * @param <T> the type of instances to build. - * @param <B> the type of builder subclass. + * <table> + * <caption>Origin support matrix</caption> + * <thead> + * <tr> + * <th>Origin Type</th> + * <th>byte[]</th> + * <th>CS</th> + * <th>File</th> + * <th>Path</th> + * <th>RAF</th> + * <th>IS</th> + * <th>Reader</th> + * <th>RBC</th> + * <th>OS</th> + * <th>Writer</th> + * <th>WBC</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td>byte[]</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * </tr> + * <tr> + * <td>CharSequence (CS)</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔<sup>1</sup></td> + * <td>✔</td> + * <td>✔<sup>1</sup></td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * </tr> + * <tr> + * <td>File</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * </tr> + * <tr> + * <td>Path</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * </tr> + * <tr> + * <td>IORandomAccessFile</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * </tr> + * <tr> + * <td>RandomAccessFile (RAF)</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * </tr> + * <tr> + * <td>InputStream (IS)</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * </tr> + * <tr> + * <td>Reader</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔<sup>1</sup></td> + * <td>✔</td> + * <td>✔<sup>1</sup></td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * </tr> + * <tr> + * <td>ReadableByteChannel (RBC)</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * </tr> + * <tr> + * <td>OutputStream (OS)</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * </tr> + * <tr> + * <td>Writer</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔<sup>1</sup></td> + * <td>✔</td> + * <td>✔<sup>1</sup></td> + * </tr> + * <tr> + * <td>WritableByteChannel (WBC)</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * </tr> + * <tr> + * <td>URI (FileSystem)</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * </tr> + * <tr> + * <td>URI (http/https))</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * <td>✔</td> + * <td>✔</td> + * <td>✔</td> + * <td>✖</td> + * <td>✖</td> + * <td>✖</td> + * </tr> + * </tbody> + * </table> + * + * <p><strong>Legend</strong></p> + * <ul> + * <li>✔ = Supported</li> + * <li>✖ = Not supported (throws {@link UnsupportedOperationException})</li> + * <li><sup>1</sup> = Characters are converted to bytes using the default {@link Charset}.</li> + * </ul> + * + * @param <T> the type produced by the builder. + * @param <B> the concrete builder subclass type. * @since 2.12.0 */ public abstract class AbstractOrigin<T, B extends AbstractOrigin<T, B>> extends AbstractSupplier<T, B> { @@ -137,6 +372,16 @@ public Writer getWriter(final Charset charset, final OpenOption... options) thro return new OutputStreamWriter(getOutputStream(options), Charsets.toCharset(charset)); } + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + return getRandomAccessFile(options).getChannel(); + } + + @Override + public WritableByteChannel getWritableByteChannel(OpenOption... options) throws IOException { + return getRandomAccessFile(options).getChannel(); + } + @Override public long size() throws IOException { return origin.length(); @@ -171,7 +416,7 @@ public byte[] getByteArray() { */ @Override public InputStream getInputStream(final OpenOption... options) throws IOException { - return new ByteArrayInputStream(origin); + return new ByteArrayInputStream(getByteArray()); } @Override @@ -179,11 +424,15 @@ public Reader getReader(final Charset charset) throws IOException { return new InputStreamReader(getInputStream(), Charsets.toCharset(charset)); } + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + return ByteArrayChannel.wrap(getByteArray()); + } + @Override public long size() throws IOException { return origin.length; } - } /** @@ -203,7 +452,7 @@ public CharSequenceOrigin(final CharSequence origin) { @Override public byte[] getByteArray() { // TODO Pass in a Charset? Consider if call sites actually need this. - return origin.toString().getBytes(Charset.defaultCharset()); + return getCharSequence(null).toString().getBytes(Charset.defaultCharset()); } /** @@ -227,7 +476,7 @@ public CharSequence getCharSequence(final Charset charset) { @Override public InputStream getInputStream(final OpenOption... options) throws IOException { // TODO Pass in a Charset? Consider if call sites actually need this. - return CharSequenceInputStream.builder().setCharSequence(getCharSequence(Charset.defaultCharset())).get(); + return CharSequenceInputStream.builder().setCharSequence(getCharSequence(null)).get(); } /** @@ -238,7 +487,12 @@ public InputStream getInputStream(final OpenOption... options) throws IOExceptio */ @Override public Reader getReader(final Charset charset) throws IOException { - return new CharSequenceReader(get()); + return new CharSequenceReader(getCharSequence(charset)); + } + + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + return ByteArrayChannel.wrap(getByteArray()); } @Override @@ -267,7 +521,7 @@ public FileOrigin(final File origin) { @Override public byte[] getByteArray(final long position, final int length) throws IOException { - try (RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(origin)) { + try (RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(getFile())) { return RandomAccessFiles.read(raf, position, length); } } @@ -280,9 +534,18 @@ public File getFile() { @Override public Path getPath() { - return get().toPath(); + return getFile().toPath(); + } + + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + return Files.newByteChannel(getPath(), options); } + @Override + public WritableByteChannel getWritableByteChannel(OpenOption... options) throws IOException { + return Files.newByteChannel(getPath(), options); + } } /** @@ -304,7 +567,7 @@ public InputStreamOrigin(final InputStream origin) { @Override public byte[] getByteArray() throws IOException { - return IOUtils.toByteArray(origin); + return IOUtils.toByteArray(getInputStream()); } /** @@ -324,6 +587,18 @@ public Reader getReader(final Charset charset) throws IOException { return new InputStreamReader(getInputStream(), Charsets.toCharset(charset)); } + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + return Channels.newChannel(getInputStream(options)); + } + + public long size() throws IOException { + if (origin instanceof FileInputStream) { + final FileInputStream fileInputStream = (FileInputStream) origin; + return fileInputStream.getChannel().size(); + } + throw unsupportedOperation("size"); + } } /** @@ -392,7 +667,12 @@ public OutputStream getOutputStream(final OpenOption... options) { */ @Override public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException { - return new OutputStreamWriter(origin, Charsets.toCharset(charset)); + return new OutputStreamWriter(getOutputStream(options), Charsets.toCharset(charset)); + } + + @Override + public WritableByteChannel getWritableByteChannel(OpenOption... options) throws IOException { + return Channels.newChannel(getOutputStream(options)); } } @@ -415,12 +695,12 @@ public PathOrigin(final Path origin) { @Override public byte[] getByteArray(final long position, final int length) throws IOException { - return RandomAccessFileMode.READ_ONLY.apply(origin, raf -> RandomAccessFiles.read(raf, position, length)); + return RandomAccessFileMode.READ_ONLY.apply(getPath(), raf -> RandomAccessFiles.read(raf, position, length)); } @Override public File getFile() { - return get().toFile(); + return getPath().toFile(); } @Override @@ -429,6 +709,15 @@ public Path getPath() { return get(); } + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + return Files.newByteChannel(getPath(), options); + } + + @Override + public WritableByteChannel getWritableByteChannel(OpenOption... options) throws IOException { + return Files.newByteChannel(getPath(), options); + } } /** @@ -474,7 +763,7 @@ public ReaderOrigin(final Reader origin) { @Override public byte[] getByteArray() throws IOException { // TODO Pass in a Charset? Consider if call sites actually need this. - return IOUtils.toByteArray(origin, Charset.defaultCharset()); + return IOUtils.toByteArray(getReader(null), Charset.defaultCharset()); } /** @@ -485,7 +774,7 @@ public byte[] getByteArray() throws IOException { */ @Override public CharSequence getCharSequence(final Charset charset) throws IOException { - return IOUtils.toString(origin); + return IOUtils.toString(getReader(charset)); } /** @@ -497,7 +786,7 @@ public CharSequence getCharSequence(final Charset charset) throws IOException { @Override public InputStream getInputStream(final OpenOption... options) throws IOException { // TODO Pass in a Charset? Consider if call sites actually need this. - return ReaderInputStream.builder().setReader(origin).setCharset(Charset.defaultCharset()).get(); + return ReaderInputStream.builder().setReader(getReader(null)).setCharset(Charset.defaultCharset()).get(); } /** @@ -511,6 +800,11 @@ public Reader getReader(final Charset charset) throws IOException { // No conversion return get(); } + + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + return Channels.newChannel(getInputStream()); + } } /** @@ -539,10 +833,6 @@ public File getFile() { public InputStream getInputStream(final OpenOption... options) throws IOException { final URI uri = get(); final String scheme = uri.getScheme(); - final FileSystemProvider fileSystemProvider = FileSystemProviders.installed().getFileSystemProvider(scheme); - if (fileSystemProvider != null) { - return Files.newInputStream(fileSystemProvider.getPath(uri), options); - } if (SCHEME_HTTP.equalsIgnoreCase(scheme) || SCHEME_HTTPS.equalsIgnoreCase(scheme)) { return uri.toURL().openStream(); } @@ -553,6 +843,21 @@ public InputStream getInputStream(final OpenOption... options) throws IOExceptio public Path getPath() { return Paths.get(get()); } + + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + final URI uri = get(); + final String scheme = uri.getScheme(); + if (SCHEME_HTTP.equalsIgnoreCase(scheme) || SCHEME_HTTPS.equalsIgnoreCase(scheme)) { + return Channels.newChannel(uri.toURL().openStream()); + } + return Files.newByteChannel(getPath(), options); + } + + @Override + public WritableByteChannel getWritableByteChannel(OpenOption... options) throws IOException { + return Files.newByteChannel(getPath(), options); + } } /** @@ -581,7 +886,7 @@ public WriterOrigin(final Writer origin) { @Override public OutputStream getOutputStream(final OpenOption... options) throws IOException { // TODO Pass in a Charset? Consider if call sites actually need this. - return WriterOutputStream.builder().setWriter(origin).setCharset(Charset.defaultCharset()).get(); + return WriterOutputStream.builder().setWriter(getWriter(null)).setCharset(Charset.defaultCharset()).get(); } /** @@ -598,6 +903,79 @@ public Writer getWriter(final Charset charset, final OpenOption... options) thro // No conversion return get(); } + + @Override + public WritableByteChannel getWritableByteChannel(OpenOption... options) throws IOException { + return Channels.newChannel(getOutputStream()); + } + } + + /** + * A {@link Channel} origin. + * + * @since 2.21.0 + */ + public static class ChannelOrigin extends AbstractOrigin<Channel, ChannelOrigin> { + + /** + * Constructs a new instance for the given origin. + * + * @param origin The origin, not null. + */ + public ChannelOrigin(final Channel origin) { + super(origin); + } + + @Override + public byte[] getByteArray() throws IOException { + return IOUtils.toByteArray(getInputStream()); + } + + @Override + public InputStream getInputStream(final OpenOption... options) throws IOException { + return Channels.newInputStream(getReadableByteChannel(options)); + } + + @Override + public Reader getReader(Charset charset) throws IOException { + return Channels.newReader( + getReadableByteChannel(), Charsets.toCharset(charset).newDecoder(), -1); + } + + @Override + public OutputStream getOutputStream(final OpenOption... options) throws IOException { + return Channels.newOutputStream(getWritableByteChannel(options)); + } + + @Override + public Writer getWriter(Charset charset, OpenOption... options) throws IOException { + return Channels.newWriter( + getWritableByteChannel(options), Charsets.toCharset(charset).newEncoder(), -1); + } + + @Override + public ReadableByteChannel getReadableByteChannel(OpenOption... options) throws IOException { + if (origin instanceof ReadableByteChannel) { + return (ReadableByteChannel) origin; + } + throw unsupportedOperation("getReadableByteChannel"); + } + + @Override + public WritableByteChannel getWritableByteChannel(OpenOption... options) throws IOException { + if (origin instanceof WritableByteChannel) { + return (WritableByteChannel) origin; + } + throw unsupportedOperation("getWritableByteChannel"); + } + + @Override + public long size() throws IOException { + if (origin instanceof SeekableByteChannel) { + return ((SeekableByteChannel) origin).size(); + } + throw unsupportedOperation("size"); + } } /** @@ -675,8 +1053,7 @@ public CharSequence getCharSequence(final Charset charset) throws IOException { * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass. */ public File getFile() { - throw new UnsupportedOperationException( - String.format("%s#getFile() for %s origin %s", getSimpleClassName(), origin.getClass().getSimpleName(), origin)); + throw unsupportedOperation("getFile"); } /** @@ -710,8 +1087,7 @@ public OutputStream getOutputStream(final OpenOption... options) throws IOExcept * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass. */ public Path getPath() { - throw new UnsupportedOperationException( - String.format("%s#getPath() for %s origin %s", getSimpleClassName(), origin.getClass().getSimpleName(), origin)); + throw unsupportedOperation("getPath"); } /** @@ -755,6 +1131,32 @@ public Writer getWriter(final Charset charset, final OpenOption... options) thro return Files.newBufferedWriter(getPath(), Charsets.toCharset(charset), options); } + /** + * Gets this origin as a ReadableByteChannel, if possible. + * + * @param options options specifying how a file-based origin is opened, ignored otherwise. + * @return a new ReadableByteChannel on the origin. + * @throws IOException if an I/O error occurs. + * @throws UnsupportedOperationException if this origin cannot be converted to a ReadableByteChannel. + * @since 2.21.0 + */ + public ReadableByteChannel getReadableByteChannel(final OpenOption... options) throws IOException { + return Channels.newChannel(getInputStream(options)); + } + + /** + * Gets this origin as a WritableByteChannel, if possible. + * + * @param options options specifying how a file-based origin is opened, ignored otherwise. + * @return a new WritableByteChannel on the origin. + * @throws IOException if an I/O error occurs. + * @throws UnsupportedOperationException if this origin cannot be converted to a WritableByteChannel. + * @since 2.21.0 + */ + public WritableByteChannel getWritableByteChannel(final OpenOption... options) throws IOException { + return Channels.newChannel(getOutputStream(options)); + } + /** * Gets the size of the origin, if possible. * @@ -770,4 +1172,10 @@ public long size() throws IOException { public String toString() { return getSimpleClassName() + "[" + origin.toString() + "]"; } + + UnsupportedOperationException unsupportedOperation(String method) { + return new UnsupportedOperationException(String.format( + "%s#%s() for %s origin %s", + getSimpleClassName(), method, origin.getClass().getSimpleName(), origin)); + } } 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 8f2354d95..b5c8f6574 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java +++ b/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java @@ -24,11 +24,15 @@ import java.io.Reader; import java.io.Writer; import java.net.URI; +import java.nio.channels.Channel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.file.Path; import java.nio.file.Paths; import org.apache.commons.io.IORandomAccessFile; import org.apache.commons.io.build.AbstractOrigin.ByteArrayOrigin; +import org.apache.commons.io.build.AbstractOrigin.ChannelOrigin; import org.apache.commons.io.build.AbstractOrigin.CharSequenceOrigin; import org.apache.commons.io.build.AbstractOrigin.FileOrigin; import org.apache.commons.io.build.AbstractOrigin.IORandomAccessFileOrigin; @@ -182,6 +186,10 @@ protected static WriterOrigin newWriterOrigin(final Writer origin) { return new WriterOrigin(origin); } + private static ChannelOrigin newChannelOrigin(final Channel origin) { + return new ChannelOrigin(origin); + } + /** * The underlying origin. */ @@ -368,4 +376,26 @@ public B setURI(final URI origin) { public B setWriter(final Writer origin) { return setOrigin(newWriterOrigin(origin)); } + + /** + * Sets a new origin. + * + * @param origin the new origin. + * @return {@code this} instance. + * @since 2.21.0 + */ + public B setReadableByteChannel(final ReadableByteChannel origin) { + return setOrigin(newChannelOrigin(origin)); + } + + /** + * Sets a new origin. + * + * @param origin the new origin. + * @return {@code this} instance. + * @since 2.21.0 + */ + public B setWritableByteChannel(final WritableByteChannel origin) { + return setOrigin(newChannelOrigin(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 3feac4a65..45344ce46 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java +++ b/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java @@ -24,6 +24,8 @@ import java.io.RandomAccessFile; import java.io.Reader; import java.io.Writer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -258,6 +260,36 @@ public Writer getWriter() throws IOException { return checkOrigin().getWriter(getCharset(), getOpenOptions()); } + /** + * Gets a ReadableByteChannel from the origin with OpenOption[]. + * + * @return A ReadableByteChannel. + * @throws IllegalStateException if the {@code origin} is {@code null}. + * @throws UnsupportedOperationException if the origin cannot be converted to a {@link ReadableByteChannel}. + * @throws IOException if an I/O error occurs. + * @see AbstractOrigin#getReadableByteChannel(OpenOption...) + * @see #getOpenOptions() + * @since 2.21.0 + */ + public ReadableByteChannel getReadableByteChannel() throws IOException { + return checkOrigin().getReadableByteChannel(getOpenOptions()); + } + + /** + * Gets a WritableByteChannel from the origin with OpenOption[]. + * + * @return A WritableByteChannel. + * @throws IllegalStateException if the {@code origin} is {@code null}. + * @throws UnsupportedOperationException if the origin cannot be converted to a {@link WritableByteChannel}. + * @throws IOException if an I/O error occurs. + * @see AbstractOrigin#getWritableByteChannel(OpenOption...) + * @see #getOpenOptions() + * @since 2.21.0 + */ + public WritableByteChannel getWritableByteChannel() throws IOException { + return checkOrigin().getWritableByteChannel(getOpenOptions()); + } + /** * Sets the buffer size. Invalid input (bufferSize <= 0) resets the value to its default. * <p> diff --git a/src/main/java/org/apache/commons/io/channels/ByteArrayChannel.java b/src/main/java/org/apache/commons/io/channels/ByteArrayChannel.java new file mode 100644 index 000000000..96767c553 --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/ByteArrayChannel.java @@ -0,0 +1,241 @@ +/* + * 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 + * + * https://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.io.channels; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SeekableByteChannel; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.commons.io.IOUtils; + +/** + * An in-memory {@link SeekableByteChannel} backed by a growable {@code byte[]} buffer. + * + * @since 2.21.0 + */ +public class ByteArrayChannel implements SeekableByteChannel { + + private static final int DEFAULT_INITIAL_CAPACITY = 32; + + /** + * Constructs a channel that wraps the given byte array. + * <p> + * The resulting channel will share the given array as its buffer, until a write operation + * requires a larger capacity. + * The initial size of the channel is the length of the given array, and the initial position is 0. + * </p> + * @param bytes The byte array to wrap; must not be {@code null}. + * @return A new channel that wraps the given byte array; never {@code null}. + * @throws NullPointerException If the byte array is {@code null}. + */ + public static ByteArrayChannel wrap(byte[] bytes) { + Objects.requireNonNull(bytes, "bytes"); + return new ByteArrayChannel(bytes, bytes.length); + } + + // package-private for testing + byte[] data; + private int position; + private int count; + private volatile boolean closed; + private final ReentrantLock lock = new ReentrantLock(); + + /** + * Constructs a channel with the default initial capacity. + * <p> + * The initial size is 0, and the initial position is 0. + * </p> + */ + public ByteArrayChannel() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * Constructs a channel with the given initial capacity. + * <p> + * The initial size is 0, and the initial position is 0. + * </p> + * @param initialCapacity The initial capacity; must be non-negative. + * @throws IllegalArgumentException If the initial capacity is negative. + */ + public ByteArrayChannel(int initialCapacity) { + this(byteArray(initialCapacity), 0); + } + + private static byte[] byteArray(int value) { + if (value < 0) { + throw new IllegalArgumentException("Size must be non-negative"); + } + return new byte[value]; + } + + private ByteArrayChannel(byte[] data, int count) { + this.data = data; + this.position = 0; + this.count = count; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + ensureOpen(); + lock.lock(); + try { + final int remaining = dst.remaining(); + if (remaining == 0) { + return 0; + } + if (position >= count) { + return -1; // EOF + } + final int n = Math.min(count - position, remaining); + dst.put(data, position, n); + position += n; + return n; + } finally { + lock.unlock(); + } + } + + @Override + public int write(ByteBuffer src) throws IOException { + ensureOpen(); + lock.lock(); + try { + final int remaining = src.remaining(); + if (remaining == 0) { + return 0; + } + final int newPosition = position + remaining; + ensureCapacity(newPosition); + src.get(data, position, remaining); + position = newPosition; + if (newPosition > count) { + count = newPosition; + } + return remaining; + } finally { + lock.unlock(); + } + } + + @Override + public long position() throws IOException { + ensureOpen(); + lock.lock(); + try { + return position; + } finally { + lock.unlock(); + } + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + if (newPosition < 0L || newPosition > IOUtils.SOFT_MAX_ARRAY_LENGTH) { + throw new IOException("position must be in range [0, " + IOUtils.SOFT_MAX_ARRAY_LENGTH + "]"); + } + ensureOpen(); + lock.lock(); + try { + this.position = (int) newPosition; // allowed to be > count; reads will return -1 + return this; + } finally { + lock.unlock(); + } + } + + @Override + public long size() throws IOException { + ensureOpen(); + lock.lock(); + try { + return count; + } finally { + lock.unlock(); + } + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + if (size < 0L || size > IOUtils.SOFT_MAX_ARRAY_LENGTH) { + throw new IOException("size must be in range [0, " + IOUtils.SOFT_MAX_ARRAY_LENGTH + "]"); + } + ensureOpen(); + lock.lock(); + try { + final int newSize = (int) size; + if (newSize < count) { + // shrink logical size; do not allocate + count = newSize; + + } + if (newSize < position) { + position = newSize; + } + // if newSize >= count: no effect + return this; + } finally { + lock.unlock(); + } + } + + @Override + public boolean isOpen() { + return !closed; + } + + @Override + public void close() { + closed = true; + } + + private void ensureOpen() throws ClosedChannelException { + if (closed) { + throw new ClosedChannelException(); + } + } + + private void ensureCapacity(int minCapacity) { + // Guard against integer overflow and against exceeding the soft maximum. + // Negative values signal overflow in the (position + remaining) arithmetic. + if (minCapacity < 0 || minCapacity > IOUtils.SOFT_MAX_ARRAY_LENGTH) { + throw new OutOfMemoryError("required array size " + minCapacity + " too large"); + } + // The current buffer is already big enough. + if (minCapacity <= data.length) { + return; + } + // Increase capacity geometrically (double the current size) to reduce reallocation cost. + // Always honor the requested minimum; if doubling overflows, use the minimum instead. + final int newCapacity = Math.max(data.length << 1, minCapacity); + // If geometric growth overshoots the soft maximum (but still fits in int), + // clamp to the soft maximum. minCapacity has already been validated to be ≤ soft max. + data = Arrays.copyOf(data, Math.min(newCapacity, IOUtils.SOFT_MAX_ARRAY_LENGTH)); + } + + /** + * Returns a copy of the logical contents. + * @return A copy of the logical contents, never {@code null}. + */ + public byte[] toByteArray() { + return Arrays.copyOf(data, count); + } +} diff --git a/src/test/java/org/apache/commons/io/build/AbstractOriginTest.java b/src/test/java/org/apache/commons/io/build/AbstractOriginTest.java index e55da41d3..430ed4e43 100644 --- a/src/test/java/org/apache/commons/io/build/AbstractOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/AbstractOriginTest.java @@ -30,18 +30,24 @@ import java.io.RandomAccessFile; import java.io.Reader; import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Objects; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractOrigin.RandomAccessFileOrigin; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -55,15 +61,19 @@ public abstract class AbstractOriginTest<T, B extends AbstractOrigin<T, B>> { protected static final String FILE_RES_RO = "/org/apache/commons/io/test-file-20byteslength.bin"; protected static final String FILE_NAME_RO = "src/test/resources" + FILE_RES_RO; - protected static final String FILE_NAME_RW = "target/" + AbstractOriginTest.class.getSimpleName() + ".txt"; + protected static final String FILE_NAME_RW = AbstractOriginTest.class.getSimpleName() + ".txt"; private static final int RO_LENGTH = 20; protected AbstractOrigin<T, B> originRo; protected AbstractOrigin<T, B> originRw; + @TempDir + protected Path tempPath; + @BeforeEach public void beforeEach() throws IOException { setOriginRo(newOriginRo()); + resetOriginRw(); setOriginRw(newOriginRw()); } @@ -92,9 +102,21 @@ protected void setOriginRw(final AbstractOrigin<T, B> origin) { this.originRw = origin; } + protected void resetOriginRw() throws IOException { + // No-op + } + + byte[] getFixtureByteArray() throws IOException { + return IOUtils.resourceToByteArray(FILE_RES_RO); + } + + String getFixtureString() throws IOException { + return IOUtils.resourceToString(FILE_RES_RO, StandardCharsets.UTF_8); + } + @Test void testGetByteArray() throws IOException { - assertArrayEquals(Files.readAllBytes(Paths.get(FILE_NAME_RO)), getOriginRo().getByteArray()); + assertArrayEquals(getFixtureByteArray(), getOriginRo().getByteArray()); } @Test @@ -114,7 +136,9 @@ void testGetByteArrayAt_1_1() throws IOException { @Test void testGetCharSequence() throws IOException { - assertNotNull(getOriginRo().getCharSequence(Charset.defaultCharset())); + final CharSequence charSequence = getOriginRo().getCharSequence(StandardCharsets.UTF_8); + assertNotNull(charSequence); + assertEquals(getFixtureString(), charSequence.toString()); } @Test @@ -135,6 +159,7 @@ private void testGetFile(final File file, final long expectedLen) throws IOExcep void testGetInputStream() throws IOException { try (InputStream inputStream = getOriginRo().getInputStream()) { assertNotNull(inputStream); + assertArrayEquals(getFixtureByteArray(), IOUtils.toByteArray(inputStream)); } } @@ -224,9 +249,15 @@ void testGetReader() throws IOException { try (Reader reader = getOriginRo().getReader(Charset.defaultCharset())) { assertNotNull(reader); } + setOriginRo(newOriginRo()); try (Reader reader = getOriginRo().getReader(null)) { assertNotNull(reader); } + setOriginRo(newOriginRo()); + try (Reader reader = getOriginRo().getReader(StandardCharsets.UTF_8)) { + assertNotNull(reader); + assertEquals(getFixtureString(), IOUtils.toString(reader)); + } } @Test @@ -240,8 +271,92 @@ void testGetWriter() throws IOException { } } + @Test + void testGetReadableByteChannel() throws IOException { + try (ReadableByteChannel channel = getOriginRo().getReadableByteChannel(StandardOpenOption.READ)) { + final SeekableByteChannel seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + assertNotNull(channel); + assertTrue(channel.isOpen()); + if (seekable != null) { + assertEquals(0, seekable.position()); + assertEquals(RO_LENGTH, seekable.size()); + } + checkRead(channel); + if (seekable != null) { + assertEquals(RO_LENGTH, seekable.position()); + } + } + } + + private void checkRead(ReadableByteChannel channel) throws IOException { + final ByteBuffer buffer = ByteBuffer.allocate(RO_LENGTH); + int read = channel.read(buffer); + assertEquals(RO_LENGTH, read); + assertArrayEquals(getFixtureByteArray(), buffer.array()); + // Channel is at EOF + buffer.clear(); + read = channel.read(buffer); + assertEquals(-1, read); + } + + @Test + void testGetWritableByteChannel() throws IOException { + testGetWritableByteChannel(true); + } + + void testGetWritableByteChannel(boolean supportsRead) throws IOException { + try (WritableByteChannel channel = getOriginRw().getWritableByteChannel(StandardOpenOption.WRITE)) { + final SeekableByteChannel seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + assertNotNull(channel); + assertTrue(channel.isOpen()); + if (seekable != null) { + assertEquals(0, seekable.position()); + assertEquals(0, seekable.size()); + } + checkWrite(channel); + if (seekable != null) { + assertEquals(RO_LENGTH, seekable.position()); + assertEquals(RO_LENGTH, seekable.size()); + } + } + if (supportsRead) { + setOriginRw(newOriginRw()); + try (ReadableByteChannel channel = getOriginRw().getReadableByteChannel(StandardOpenOption.READ)) { + assertNotNull(channel); + assertTrue(channel.isOpen()); + checkRead(channel); + } + } + setOriginRw(newOriginRw()); + try (WritableByteChannel channel = getOriginRw().getWritableByteChannel(StandardOpenOption.WRITE)) { + final SeekableByteChannel seekable = + channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + assertNotNull(channel); + assertTrue(channel.isOpen()); + if (seekable != null) { + seekable.position(RO_LENGTH); + assertEquals(RO_LENGTH, seekable.position()); + assertEquals(RO_LENGTH, seekable.size()); + // Truncate + final int newSize = RO_LENGTH / 2; + seekable.truncate(newSize); + assertEquals(newSize, seekable.position()); + assertEquals(newSize, seekable.size()); + // Rewind + seekable.position(0); + assertEquals(0, seekable.position()); + } + } + } + + private void checkWrite(WritableByteChannel channel) throws IOException { + final ByteBuffer buffer = ByteBuffer.wrap(getFixtureByteArray()); + final int written = channel.write(buffer); + assertEquals(RO_LENGTH, written); + } + @Test void testSize() throws IOException { - assertEquals(Files.size(Paths.get(FILE_NAME_RO)), getOriginRo().getByteArray().length); + assertEquals(RO_LENGTH, getOriginRo().getByteArray().length); } } diff --git a/src/test/java/org/apache/commons/io/build/AbstractRandomAccessFileOriginTest.java b/src/test/java/org/apache/commons/io/build/AbstractRandomAccessFileOriginTest.java index 369ce295d..28644f3c8 100644 --- a/src/test/java/org/apache/commons/io/build/AbstractRandomAccessFileOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/AbstractRandomAccessFileOriginTest.java @@ -17,12 +17,17 @@ package org.apache.commons.io.build; +import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import org.apache.commons.io.IORandomAccessFile; import org.apache.commons.io.build.AbstractOrigin.AbstractRandomAccessFileOrigin; import org.apache.commons.io.build.AbstractOrigin.IORandomAccessFileOrigin; import org.apache.commons.io.build.AbstractOrigin.RandomAccessFileOrigin; +import org.apache.commons.lang3.ArrayUtils; /** * Tests {@link RandomAccessFileOrigin} and {@link IORandomAccessFileOrigin}. @@ -35,4 +40,10 @@ public abstract class AbstractRandomAccessFileOriginTest<T extends RandomAccessFile, B extends AbstractRandomAccessFileOrigin<T, B>> extends AbstractOriginTest<T, B> { + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); + } } diff --git a/src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java b/src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java index f94957f53..a90c507c7 100644 --- a/src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java +++ b/src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java @@ -17,13 +17,31 @@ package org.apache.commons.io.build; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.FileInputStream; +import java.io.RandomAccessFile; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; +import org.apache.commons.io.function.IOConsumer; +import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; /** * Tests {@link AbstractStreamBuilder}. @@ -65,4 +83,52 @@ void testBufferSizeChecker() { // resize assertResult(builder().setBufferSizeMax(2).setBufferSizeChecker(i -> 100).setBufferSize(3).get(), 100); } + + private static Stream<IOConsumer<Builder>> fileBasedConfigurers() throws URISyntaxException { + final URI uri = Objects.requireNonNull( + AbstractStreamBuilderTest.class.getResource(AbstractOriginTest.FILE_RES_RO)) + .toURI(); + final Path path = Paths.get(AbstractOriginTest.FILE_NAME_RO); + return Stream.of( + b -> b.setByteArray(ArrayUtils.EMPTY_BYTE_ARRAY), + b -> b.setFile(AbstractOriginTest.FILE_NAME_RO), + b -> b.setFile(path.toFile()), + b -> b.setPath(AbstractOriginTest.FILE_NAME_RO), + b -> b.setPath(path), + b -> b.setRandomAccessFile(new RandomAccessFile(AbstractOriginTest.FILE_NAME_RO, "r")), + // We can convert FileInputStream to ReadableByteChannel, but not the reverse. + // Therefore, we don't use Files.newInputStream. + b -> b.setInputStream(new FileInputStream(AbstractOriginTest.FILE_NAME_RO)), + b -> b.setReadableByteChannel(Files.newByteChannel(path)), + b -> b.setURI(uri)); + } + + /** + * Tests various ways to obtain a {@link java.io.InputStream}. + * + * @param configurer Lambda to configure the builder. + */ + @ParameterizedTest + @MethodSource("fileBasedConfigurers") + void testGetInputStream(IOConsumer<Builder> configurer) throws Exception { + final Builder builder = builder(); + configurer.accept(builder); + assertNotNull(builder.getInputStream()); + } + + /** + * Tests various ways to obtain a {@link SeekableByteChannel}. + * + * @param configurer Lambda to configure the builder. + */ + @ParameterizedTest + @MethodSource("fileBasedConfigurers") + void getGetSeekableByteChannel(IOConsumer<Builder> configurer) throws Exception { + final Builder builder = builder(); + configurer.accept(builder); + try (ReadableByteChannel channel = assertDoesNotThrow(builder::getReadableByteChannel)) { + assertTrue(channel.isOpen()); + assertInstanceOf(SeekableByteChannel.class, channel); + } + } } diff --git a/src/test/java/org/apache/commons/io/build/ByteArrayOriginTest.java b/src/test/java/org/apache/commons/io/build/ByteArrayOriginTest.java index 41a693148..c23fc0cf1 100644 --- a/src/test/java/org/apache/commons/io/build/ByteArrayOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/ByteArrayOriginTest.java @@ -53,13 +53,6 @@ void testGetFile() { assertThrows(UnsupportedOperationException.class, super::testGetFile); } - @Override - @Test - void testGetOutputStream() { - // Cannot convert a byte[] to an OutputStream. - assertThrows(UnsupportedOperationException.class, super::testGetOutputStream); - } - @Override @Test void testGetPath() { @@ -82,6 +75,13 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetOutputStream() { + // Cannot convert a byte[] to an OutputStream. + assertThrows(UnsupportedOperationException.class, super::testGetOutputStream); + } + @Override @Test void testGetWriter() { @@ -89,4 +89,10 @@ void testGetWriter() { assertThrows(UnsupportedOperationException.class, super::testGetWriter); } + @Override + @Test + void testGetWritableByteChannel() throws IOException { + // Cannot convert a byte[] to a WritableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetWritableByteChannel); + } } diff --git a/src/test/java/org/apache/commons/io/build/ChannelOriginTest.java b/src/test/java/org/apache/commons/io/build/ChannelOriginTest.java new file mode 100644 index 000000000..227f9ae88 --- /dev/null +++ b/src/test/java/org/apache/commons/io/build/ChannelOriginTest.java @@ -0,0 +1,108 @@ +/* + * 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 + * + * https://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.io.build; + +import static java.nio.file.StandardOpenOption.READ; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.apache.commons.io.build.AbstractOrigin.ChannelOrigin; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class ChannelOriginTest extends AbstractOriginTest<Channel, ChannelOrigin> { + @Override + protected ChannelOrigin newOriginRo() throws IOException { + return new ChannelOrigin(Files.newByteChannel(Paths.get(FILE_NAME_RO), Collections.singleton(READ))); + } + + @Override + protected ChannelOrigin newOriginRw() throws IOException { + return new ChannelOrigin(Files.newByteChannel( + tempPath.resolve(FILE_NAME_RW), + new HashSet<>(Arrays.asList(StandardOpenOption.READ, StandardOpenOption.WRITE)))); + } + + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); + } + + @Override + @Test + void testGetFile() { + // A FileByteChannel cannot be converted into a File. + assertThrows(UnsupportedOperationException.class, super::testGetFile); + } + + @Override + @Test + void testGetPath() { + // A FileByteChannel cannot be converted into a Path. + assertThrows(UnsupportedOperationException.class, super::testGetPath); + } + + @Override + @Test + void testGetRandomAccessFile() { + // A FileByteChannel cannot be converted into a RandomAccessFile. + assertThrows(UnsupportedOperationException.class, super::testGetRandomAccessFile); + } + + @Override + @ParameterizedTest + @EnumSource(StandardOpenOption.class) + void testGetRandomAccessFile(OpenOption openOption) { + // A FileByteChannel cannot be converted into a RandomAccessFile. + assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); + } + + @Test + void testUnsupportedOperations_ReadableByteChannel() { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + final ChannelOrigin origin = new ChannelOrigin(channel); + assertThrows(UnsupportedOperationException.class, origin::getOutputStream); + assertThrows(UnsupportedOperationException.class, () -> origin.getWriter(null)); + assertThrows(UnsupportedOperationException.class, origin::getWritableByteChannel); + } + + @Test + void testUnsupportedOperations_WritableByteChannel() { + final Channel channel = mock(WritableByteChannel.class); + final ChannelOrigin origin = new ChannelOrigin(channel); + assertThrows(UnsupportedOperationException.class, origin::getInputStream); + assertThrows(UnsupportedOperationException.class, () -> origin.getReader(null)); + assertThrows(UnsupportedOperationException.class, origin::getReadableByteChannel); + } +} diff --git a/src/test/java/org/apache/commons/io/build/CharSequenceOriginTest.java b/src/test/java/org/apache/commons/io/build/CharSequenceOriginTest.java index df29f453a..b6e768b95 100644 --- a/src/test/java/org/apache/commons/io/build/CharSequenceOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/CharSequenceOriginTest.java @@ -62,13 +62,6 @@ void testGetFile() { assertThrows(UnsupportedOperationException.class, super::testGetFile); } - @Override - @Test - void testGetOutputStream() { - // Cannot convert a CharSequence to an OutputStream. - assertThrows(UnsupportedOperationException.class, super::testGetOutputStream); - } - @Override @Test void testGetPath() { @@ -91,6 +84,27 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetOutputStream() { + // Cannot convert a CharSequence to an OutputStream. + assertThrows(UnsupportedOperationException.class, super::testGetOutputStream); + } + + @Override + @Test + void testGetWriter() { + // Cannot convert a CharSequence to a Writer. + assertThrows(UnsupportedOperationException.class, super::testGetWriter); + } + + @Override + @Test + void testGetWritableByteChannel() throws IOException { + // Cannot convert a CharSequence to a WritableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetWritableByteChannel); + } + @Test void testGetReaderIgnoreCharset() throws IOException { // The CharSequenceOrigin ignores the given Charset. @@ -109,11 +123,4 @@ void testGetReaderIgnoreCharsetNull() throws IOException { } } - @Override - @Test - void testGetWriter() { - // Cannot convert a CharSequence to a Writer. - assertThrows(UnsupportedOperationException.class, super::testGetWriter); - } - } diff --git a/src/test/java/org/apache/commons/io/build/FileOriginTest.java b/src/test/java/org/apache/commons/io/build/FileOriginTest.java index 4f0cb2514..548d78885 100644 --- a/src/test/java/org/apache/commons/io/build/FileOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/FileOriginTest.java @@ -17,8 +17,13 @@ package org.apache.commons.io.build; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import org.apache.commons.io.build.AbstractOrigin.FileOrigin; +import org.apache.commons.lang3.ArrayUtils; /** * Tests {@link FileOrigin}. @@ -35,8 +40,14 @@ protected FileOrigin newOriginRo() { } @Override - protected FileOrigin newOriginRw() { - return new FileOrigin(new File(FILE_NAME_RW)); + protected FileOrigin newOriginRw() throws IOException { + return new FileOrigin(tempPath.resolve(FILE_NAME_RW).toFile()); } + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); + } } diff --git a/src/test/java/org/apache/commons/io/build/IORandomAccessFileOriginTest.java b/src/test/java/org/apache/commons/io/build/IORandomAccessFileOriginTest.java index 8d83bba1c..b51118c65 100644 --- a/src/test/java/org/apache/commons/io/build/IORandomAccessFileOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/IORandomAccessFileOriginTest.java @@ -17,6 +17,7 @@ package org.apache.commons.io.build; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.RandomAccessFile; import org.apache.commons.io.IORandomAccessFile; @@ -38,8 +39,7 @@ protected IORandomAccessFileOrigin newOriginRo() throws FileNotFoundException { @SuppressWarnings("resource") @Override - protected IORandomAccessFileOrigin newOriginRw() throws FileNotFoundException { - return new IORandomAccessFileOrigin(RandomAccessFileMode.READ_WRITE.io(FILE_NAME_RW)); + protected IORandomAccessFileOrigin newOriginRw() throws IOException { + return new IORandomAccessFileOrigin(RandomAccessFileMode.READ_WRITE.io(tempPath.resolve(FILE_NAME_RW).toFile().getPath())); } - } diff --git a/src/test/java/org/apache/commons/io/build/InputStreamOriginTest.java b/src/test/java/org/apache/commons/io/build/InputStreamOriginTest.java index ac83acbec..29a6ac30d 100644 --- a/src/test/java/org/apache/commons/io/build/InputStreamOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/InputStreamOriginTest.java @@ -20,6 +20,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; @@ -58,13 +59,6 @@ void testGetFile() { assertThrows(UnsupportedOperationException.class, super::testGetFile); } - @Override - @Test - void testGetOutputStream() { - // Cannot convert a InputStream to an OutputStream. - assertThrows(UnsupportedOperationException.class, super::testGetOutputStream); - } - @Override @Test void testGetPath() { @@ -87,6 +81,13 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetOutputStream() { + // Cannot convert a InputStream to an OutputStream. + assertThrows(UnsupportedOperationException.class, super::testGetOutputStream); + } + @Override @Test void testGetWriter() { @@ -94,4 +95,10 @@ void testGetWriter() { assertThrows(UnsupportedOperationException.class, super::testGetWriter); } + @Override + @Test + void testGetWritableByteChannel() throws IOException { + // Cannot convert a InputStream to a WritableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetWritableByteChannel); + } } diff --git a/src/test/java/org/apache/commons/io/build/OutputStreamOriginTest.java b/src/test/java/org/apache/commons/io/build/OutputStreamOriginTest.java index 5e66e244d..cea4aa1bc 100644 --- a/src/test/java/org/apache/commons/io/build/OutputStreamOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/OutputStreamOriginTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; import java.io.OutputStream; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; @@ -89,13 +90,6 @@ void testGetFile() { assertThrows(UnsupportedOperationException.class, super::testGetFile); } - @Override - @Test - void testGetInputStream() { - // Cannot convert a OutputStream to an InputStream. - assertThrows(UnsupportedOperationException.class, super::testGetInputStream); - } - @Override @Test void testGetPath() { @@ -118,6 +112,13 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetInputStream() { + // Cannot convert a OutputStream to an InputStream. + assertThrows(UnsupportedOperationException.class, super::testGetInputStream); + } + @Override @Test void testGetReader() { @@ -125,6 +126,18 @@ void testGetReader() { assertThrows(UnsupportedOperationException.class, super::testGetReader); } + @Override + @Test + void testGetReadableByteChannel() throws IOException { + // Cannot convert a OutputStream to a ReadableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetReadableByteChannel); + } + + @Override + void testGetWritableByteChannel() throws IOException { + super.testGetWritableByteChannel(false); + } + @Override @Test void testSize() { diff --git a/src/test/java/org/apache/commons/io/build/PathOriginTest.java b/src/test/java/org/apache/commons/io/build/PathOriginTest.java index 81c4fc8d9..bc29df52b 100644 --- a/src/test/java/org/apache/commons/io/build/PathOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/PathOriginTest.java @@ -16,10 +16,14 @@ */ package org.apache.commons.io.build; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import org.apache.commons.io.build.AbstractOrigin.PathOrigin; +import org.apache.commons.lang3.ArrayUtils; /** * Tests {@link PathOrigin}. @@ -36,8 +40,14 @@ protected PathOrigin newOriginRo() { } @Override - protected PathOrigin newOriginRw() { - return new PathOrigin(Paths.get(FILE_NAME_RW)); + protected PathOrigin newOriginRw() throws IOException { + return new PathOrigin(tempPath.resolve(FILE_NAME_RW)); } + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); + } } diff --git a/src/test/java/org/apache/commons/io/build/RandomAccessFileOriginTest.java b/src/test/java/org/apache/commons/io/build/RandomAccessFileOriginTest.java index 7cebf778d..d7306c12c 100644 --- a/src/test/java/org/apache/commons/io/build/RandomAccessFileOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/RandomAccessFileOriginTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.RandomAccessFile; import org.apache.commons.io.RandomAccessFileMode; @@ -41,8 +42,8 @@ protected RandomAccessFileOrigin newOriginRo() throws FileNotFoundException { @SuppressWarnings("resource") @Override - protected RandomAccessFileOrigin newOriginRw() throws FileNotFoundException { - return new RandomAccessFileOrigin(RandomAccessFileMode.READ_WRITE.create(FILE_NAME_RW)); + protected RandomAccessFileOrigin newOriginRw() throws IOException { + return new RandomAccessFileOrigin(RandomAccessFileMode.READ_WRITE.create(tempPath.resolve(FILE_NAME_RW))); } @Override diff --git a/src/test/java/org/apache/commons/io/build/ReaderOriginTest.java b/src/test/java/org/apache/commons/io/build/ReaderOriginTest.java index 6897d2418..b73731932 100644 --- a/src/test/java/org/apache/commons/io/build/ReaderOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/ReaderOriginTest.java @@ -20,6 +20,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; +import java.io.IOException; import java.io.Reader; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; @@ -56,13 +57,6 @@ void testGetFile() { assertThrows(UnsupportedOperationException.class, super::testGetFile); } - @Override - @Test - void testGetOutputStream() { - // Cannot convert a Reader to an OutputStream. - assertThrows(UnsupportedOperationException.class, super::testGetOutputStream); - } - @Override @Test void testGetPath() { @@ -85,6 +79,13 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetOutputStream() { + // Cannot convert a Reader to an OutputStream. + assertThrows(UnsupportedOperationException.class, super::testGetOutputStream); + } + @Override @Test void testGetWriter() { @@ -92,4 +93,10 @@ void testGetWriter() { assertThrows(UnsupportedOperationException.class, super::testGetWriter); } + @Override + @Test + void testGetWritableByteChannel() throws IOException { + // Cannot convert a InputStream to a WritableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetWritableByteChannel); + } } diff --git a/src/test/java/org/apache/commons/io/build/URIOriginTest.java b/src/test/java/org/apache/commons/io/build/URIOriginTest.java index 303cfe111..6fd2043eb 100644 --- a/src/test/java/org/apache/commons/io/build/URIOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/URIOriginTest.java @@ -18,11 +18,16 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; +import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import org.apache.commons.io.build.AbstractOrigin.URIOrigin; +import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -43,7 +48,14 @@ protected URIOrigin newOriginRo() { @Override protected URIOrigin newOriginRw() { - return new URIOrigin(Paths.get(FILE_NAME_RW).toUri()); + return new URIOrigin(tempPath.resolve(FILE_NAME_RW).toUri()); + } + + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); } @ParameterizedTest diff --git a/src/test/java/org/apache/commons/io/build/WriterStreamOriginTest.java b/src/test/java/org/apache/commons/io/build/WriterStreamOriginTest.java index f0aa52512..5dadbe530 100644 --- a/src/test/java/org/apache/commons/io/build/WriterStreamOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/WriterStreamOriginTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.file.OpenOption; @@ -89,13 +90,6 @@ void testGetFile() { assertThrows(UnsupportedOperationException.class, super::testGetFile); } - @Override - @Test - void testGetInputStream() { - // Cannot convert a Writer to an InputStream. - assertThrows(UnsupportedOperationException.class, super::testGetInputStream); - } - @Override @Test void testGetPath() { @@ -118,6 +112,13 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetInputStream() { + // Cannot convert a Writer to an InputStream. + assertThrows(UnsupportedOperationException.class, super::testGetInputStream); + } + @Override @Test void testGetReader() { @@ -125,6 +126,17 @@ void testGetReader() { assertThrows(UnsupportedOperationException.class, super::testGetReader); } + @Override + void testGetReadableByteChannel() throws IOException { + // Cannot convert a Writer to a ReadableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetReadableByteChannel); + } + + @Override + void testGetWritableByteChannel() throws IOException { + super.testGetWritableByteChannel(false); + } + @Override @Test void testSize() { diff --git a/src/test/java/org/apache/commons/io/channels/ByteArrayChannelTest.java b/src/test/java/org/apache/commons/io/channels/ByteArrayChannelTest.java new file mode 100644 index 000000000..d456fae75 --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/ByteArrayChannelTest.java @@ -0,0 +1,241 @@ +/* + * 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 + * + * https://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.io.channels; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.lang3.ArrayUtils.EMPTY_BYTE_ARRAY; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.util.stream.Stream; + +import org.apache.commons.io.function.IOConsumer; +import org.apache.commons.io.function.IOSupplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ByteArrayChannelTest { + + private static final byte[] testData = "Some data".getBytes(UTF_8); + + private static byte[] getTestData() { + return testData.clone(); + } + + static Stream<Arguments> testConstructor() { + return Stream.of( + Arguments.of((IOSupplier<ByteArrayChannel>) ByteArrayChannel::new, EMPTY_BYTE_ARRAY, 32), + Arguments.of((IOSupplier<ByteArrayChannel>) () -> new ByteArrayChannel(8), EMPTY_BYTE_ARRAY, 8), + Arguments.of((IOSupplier<ByteArrayChannel>) () -> new ByteArrayChannel(16), EMPTY_BYTE_ARRAY, 16), + Arguments.of( + (IOSupplier<ByteArrayChannel>) () -> ByteArrayChannel.wrap(EMPTY_BYTE_ARRAY), EMPTY_BYTE_ARRAY, 0), + Arguments.of((IOSupplier<ByteArrayChannel>) () -> ByteArrayChannel.wrap(getTestData()), getTestData(), testData.length)); + } + + @ParameterizedTest + @MethodSource + void testConstructor(IOSupplier<ByteArrayChannel> supplier, byte[] expected, int capacity) throws IOException { + try (ByteArrayChannel channel = supplier.get()) { + assertEquals(0, channel.position()); + assertEquals(expected.length, channel.size()); + assertEquals(capacity, channel.data.length); + assertArrayEquals(expected, channel.toByteArray()); + } + } + + @Test + void testConstructorInvalid() { + assertThrows(IllegalArgumentException.class, () -> new ByteArrayChannel(-1)); + assertThrows(NullPointerException.class, () -> ByteArrayChannel.wrap(null)); + } + + @Test + void testCloseIdempotent() { + final ByteArrayChannel channel = new ByteArrayChannel(); + channel.close(); + assertFalse(channel.isOpen()); + channel.close(); + assertFalse(channel.isOpen()); + } + + static Stream<IOConsumer<ByteArrayChannel>> testThrowsAfterClose() { + return Stream.of( + channel -> channel.read(ByteBuffer.allocate(1)), + channel -> channel.write(ByteBuffer.allocate(1)), + ByteArrayChannel::position, + channel -> channel.position(0), + ByteArrayChannel::size, + channel -> channel.truncate(0)); + } + + @ParameterizedTest + @MethodSource + void testThrowsAfterClose(IOConsumer<ByteArrayChannel> consumer) { + final ByteArrayChannel channel = new ByteArrayChannel(); + channel.close(); + assertThrows(ClosedChannelException.class, () -> consumer.accept(channel)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 4, 9, 15}) + void testPosition(long expected) throws Exception { + try (ByteArrayChannel channel = ByteArrayChannel.wrap(getTestData())) { + assertEquals(0, channel.position(), "initial position"); + channel.position(expected); + assertEquals(expected, channel.position(), "set position"); + } + } + + @ParameterizedTest + @ValueSource(longs = {-1, Integer.MAX_VALUE - 7, Integer.MAX_VALUE + 1L}) + void testPositionInvalid(long position) { + try (ByteArrayChannel channel = ByteArrayChannel.wrap(getTestData())) { + assertThrows(IOException.class, () -> channel.position(position), "position " + position); + } + } + + static Stream<Arguments> testRead() { + return Stream.of( + Arguments.of(0, 0, "", 0), + Arguments.of(0, 4, "Some", 4), + Arguments.of(5, 9, "data", 4), + Arguments.of(0, 9, "Some data", 9), + // buffer larger than data + Arguments.of(0, 10, "Some data", 9), + // offset beyond end + Arguments.of(10, 10, "", -1)); + } + + @ParameterizedTest + @MethodSource + void testRead(int offset, int bufferSize, String expected, int expectedRead) throws Exception { + try (ByteArrayChannel channel = ByteArrayChannel.wrap(getTestData())) { + channel.position(offset); + final ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + final int read = channel.read(buffer); + assertEquals(expectedRead, read, "read"); + assertEquals(expected, new String(buffer.array(), 0, Math.max(0, read), UTF_8), "data"); + assertEquals(offset + Math.max(0, expectedRead), channel.position(), "position"); + } + } + + @Test + void testMultipleRead() throws Exception { + try (ByteArrayChannel channel = ByteArrayChannel.wrap(getTestData())) { + final ByteBuffer buffer = ByteBuffer.allocate(4); + int read = channel.read(buffer); + assertEquals(4, read, "first read"); + assertEquals("Some", new String(buffer.array(), 0, read, UTF_8), "first data"); + assertEquals(4, channel.position(), "first position"); + + buffer.clear(); + read = channel.read(buffer); + assertEquals(4, read, "second read"); + assertEquals(" dat", new String(buffer.array(), 0, read, UTF_8), "second data"); + assertEquals(8, channel.position(), "second position"); + + buffer.clear(); + read = channel.read(buffer); + assertEquals(1, read, "third read"); + assertEquals("a", new String(buffer.array(), 0, read, UTF_8), "third data"); + assertEquals(9, channel.position(), "third position"); + + buffer.clear(); + read = channel.read(buffer); + assertEquals(-1, read, "fourth read"); + assertEquals(9, channel.position(), "fourth position"); + } + } + + static Stream<Arguments> testWrite() { + return Stream.of( + Arguments.of(1, "", 0, "Some data"), + Arguments.of(0, "More", 4, "More data"), + Arguments.of(5, "doll", 4, "Some doll"), + // extend + Arguments.of(9, "!", 1, "Some data!"), + // offset beyond end + Arguments.of(12, "!!!", 3, "Some data\0\0\0!!!")); + } + + @ParameterizedTest + @MethodSource + void testWrite(int offset, String toWrite, int expectedWritten, String expectedData) throws Exception { + try (ByteArrayChannel channel = ByteArrayChannel.wrap(getTestData())) { + channel.position(offset); + final ByteBuffer buffer = ByteBuffer.wrap(toWrite.getBytes(UTF_8)); + final int written = channel.write(buffer); + assertEquals(expectedWritten, written, "written"); + assertEquals(expectedData, new String(channel.toByteArray(), UTF_8), "data"); + assertEquals(offset + expectedWritten, channel.position(), "position"); + } + } + + @Test + void testSize() throws Exception { + try (ByteArrayChannel channel = ByteArrayChannel.wrap(getTestData())) { + assertEquals(testData.length, channel.size(), "size"); + channel.position(testData.length + 10); + assertEquals(testData.length, channel.size(), "size after position beyond end"); + channel.write(ByteBuffer.wrap("More".getBytes(UTF_8))); + assertEquals(testData.length + 10 + 4, channel.size(), "size after write beyond end"); + } + } + + static Stream<Arguments> testTruncate() { + return Stream.of( + Arguments.of(0, 0, ""), + Arguments.of(0, 4, ""), + Arguments.of(4, 0, "Some"), + Arguments.of(4, 5, "Some"), + Arguments.of(9, 0, "Some data"), + Arguments.of(9, 10, "Some data"), + // extend - no effect + Arguments.of(15, 0, "Some data"), + Arguments.of(15, 20, "Some data")); + } + + @ParameterizedTest + @MethodSource + void testTruncate(int size, int initialPosition, String expectedData) throws Exception { + try (ByteArrayChannel channel = ByteArrayChannel.wrap(getTestData())) { + channel.position(initialPosition); + channel.truncate(size); + assertEquals(expectedData, new String(channel.toByteArray(), UTF_8), "data"); + // Size changes only if size < initial size + assertEquals(Math.min(size, testData.length), channel.size(), "size"); + // Position changes only if size < initial position + assertEquals(Math.min(size, initialPosition), channel.position(), "position"); + } + } + + @Test + void testTruncateInvalid() { + try (ByteArrayChannel channel = ByteArrayChannel.wrap(getTestData())) { + assertThrows(IOException.class, () -> channel.truncate(-1)); + assertThrows(IOException.class, () -> channel.truncate((long) Integer.MAX_VALUE + 1)); + } + } +}
