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 &lt;= 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));
+        }
+    }
+}


Reply via email to