This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-io.git

commit 60b61dabc7927afaa4a7205046d889a2daef2293
Author: Gary Gregory <gardgreg...@gmail.com>
AuthorDate: Sun Sep 19 21:56:28 2021 -0400

    [IO-726] Add MemoryMappedInputStream, #215.
    
    Modified PR 215 from shollander with:
    - Do not write the console.
    - Simplify cleaner check support.
    - Simplify exception handling.
    - Don't initialize instance variables to their default values.
    - Un-nest else clause.
    - Declaring interfaces as static is redundant.
    - Use EOF constant instead of magic number.
    - Don't throw RuntimeException, use IllegalStateException.
    - Pacakge private class does not need public method.
    - Use final.
    - Improve Javadocs.
    - Javadoc sentences should end in a period.
    - Javadoc do not need first sentence to be in an HTML p element.
    - No need to use `this.` when you do not need to.
    - In-line some single use variables.
    - Sort members.
---
 src/changes/changes.xml                            |   3 +
 .../io/input/BufferedFileChannelInputStream.java   |  82 +------
 .../apache/commons/io/input/ByteBufferCleaner.java | 116 ++++++++++
 .../io/input/MemoryMappedFileInputStream.java      | 177 +++++++++++++++
 .../commons/io/input/ByteBufferCleanerTest.java    |  53 +++++
 .../io/input/MemoryMappedFileInputStreamTest.java  | 244 +++++++++++++++++++++
 6 files changed, 602 insertions(+), 73 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ffb8987..ca910fa 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -74,6 +74,9 @@ The <action> type attribute can be add,update,fix,remove.
       <action dev="ggregory" type="fix" due-to="Arturo Bernal">
         Fix Javadoc in ThreadMonitor#run() method. #273.
       </action>
+      <action issue="IO-726" dev="ggregory" type="fix" due-to="shollander, 
Gary Gregory">
+        Add MemoryMappedInputStream. #215.
+      </action>
       <!-- ADD -->
       <action dev="ggregory" type="add" due-to="Gary Gregory">
         Add BrokenReader.INSTANCE.
diff --git 
a/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java 
b/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java
index 95792c8..f9e6ad7 100644
--- 
a/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java
+++ 
b/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java
@@ -18,8 +18,6 @@ import static org.apache.commons.io.IOUtils.EOF;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.file.Path;
@@ -40,29 +38,12 @@ import org.apache.commons.io.IOUtils;
  *
  * @since 2.9.0
  */
-@SuppressWarnings("restriction")
 public final class BufferedFileChannelInputStream extends InputStream {
 
     private final ByteBuffer byteBuffer;
 
     private final FileChannel fileChannel;
 
-    private static final Class<?> DIRECT_BUFFER_CLASS = getDirectBufferClass();
-
-    private static Class<?> getDirectBufferClass() {
-        Class<?> res = null;
-        try {
-            res = Class.forName("sun.nio.ch.DirectBuffer");
-        } catch (final IllegalAccessError | ClassNotFoundException ignored) {
-            // ignored
-        }
-        return res;
-    }
-
-    private static boolean isDirectBuffer(final Object object) {
-        return DIRECT_BUFFER_CLASS != null && 
DIRECT_BUFFER_CLASS.isInstance(object);
-    }
-
     /**
      * Constructs a new instance for the given File.
      *
@@ -115,75 +96,30 @@ public final class BufferedFileChannelInputStream extends 
InputStream {
 
     /**
      * Attempts to clean up a ByteBuffer if it is direct or memory-mapped. 
This uses an *unsafe* Sun API that will cause
-     * errors if one attempts to read from the disposed buffer. However, 
neither the bytes allocated to direct buffers
-     * nor file descriptors opened for memory-mapped buffers put pressure on 
the garbage collector. Waiting for garbage
+     * errors if one attempts to read from the disposed buffer. However, 
neither the bytes allocated to direct buffers nor
+     * file descriptors opened for memory-mapped buffers put pressure on the 
garbage collector. Waiting for garbage
      * collection may lead to the depletion of off-heap memory or huge numbers 
of open files. There's unfortunately no
      * standard API to manually dispose of these kinds of buffers.
      *
      * @param buffer the buffer to clean.
      */
     private void clean(final ByteBuffer buffer) {
-        if (isDirectBuffer(buffer)) {
+        if (buffer.isDirect()) {
             cleanDirectBuffer(buffer);
         }
     }
 
     /**
-     * In Java 8, the type of DirectBuffer.cleaner() was sun.misc.Cleaner, and 
it was possible to access the method
-     * sun.misc.Cleaner.clean() to invoke it. The type changed to 
jdk.internal.ref.Cleaner in later JDKs, and the
-     * .clean() method is not accessible even with reflection. However 
sun.misc.Unsafe added a invokeCleaner() method in
-     * JDK 9+ and this is still accessible with reflection.
+     * In Java 8, the type of {@code sun.nio.ch.DirectBuffer.cleaner()} was 
{@code sun.misc.Cleaner}, and it was possible to
+     * access the method {@code sun.misc.Cleaner.clean()} to invoke it. The 
type changed to {@code jdk.internal.ref.Cleaner}
+     * in later JDKs, and the {@code clean()} method is not accessible even 
with reflection. However {@code sun.misc.Unsafe}
+     * added an {@code invokeCleaner()} method in JDK 9+ and this is still 
accessible with reflection.
      *
      * @param buffer the buffer to clean. must be a DirectBuffer.
      */
     private void cleanDirectBuffer(final ByteBuffer buffer) {
-        //
-        // Ported from StorageUtils.scala.
-        //
-//      private val bufferCleaner: DirectBuffer => Unit =
-//      if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9)) {
-//        val cleanerMethod =
-//          Utils.classForName("sun.misc.Unsafe").getMethod("invokeCleaner", 
classOf[ByteBuffer])
-//        val unsafeField = classOf[Unsafe].getDeclaredField("theUnsafe")
-//        unsafeField.setAccessible(true)
-//        val unsafe = unsafeField.get(null).asInstanceOf[Unsafe]
-//        buffer: DirectBuffer => cleanerMethod.invoke(unsafe, buffer)
-//      } else {
-//        val cleanerMethod = 
Utils.classForName("sun.misc.Cleaner").getMethod("clean")
-//        buffer: DirectBuffer => {
-//          // Careful to avoid the return type of .cleaner(), which changes 
with JDK
-//          val cleaner: AnyRef = buffer.cleaner()
-//          if (cleaner != null) {
-//            cleanerMethod.invoke(cleaner)
-//          }
-//        }
-//      }
-        //
-        final String specVer = 
System.getProperty("java.specification.version");
-        if ("1.8".equals(specVer)) {
-            // On Java 8, but also compiles on Java 11.
-            try {
-              final Class<?> clsCleaner = Class.forName("sun.misc.Cleaner");
-              final Method cleanerMethod = 
DIRECT_BUFFER_CLASS.getMethod("cleaner");
-              final Object cleaner = cleanerMethod.invoke(buffer);
-              if (cleaner != null) {
-                  final Method cleanMethod = clsCleaner.getMethod("clean");
-                  cleanMethod.invoke(cleaner);
-              }
-            } catch (final ReflectiveOperationException e) {
-                throw new IllegalStateException(e);
-            }
-        } else {
-            // On Java 9 and up, but compiles on Java 8.
-            try {
-                final Class<?> clsUnsafe = Class.forName("sun.misc.Unsafe");
-                final Method cleanerMethod = 
clsUnsafe.getMethod("invokeCleaner", ByteBuffer.class);
-                final Field unsafeField = 
clsUnsafe.getDeclaredField("theUnsafe");
-                unsafeField.setAccessible(true);
-                cleanerMethod.invoke(unsafeField.get(null), buffer);
-            } catch (final ReflectiveOperationException e) {
-                throw new IllegalStateException(e);
-            }
+        if (ByteBufferCleaner.isSupported()) {
+            ByteBufferCleaner.clean(buffer);
         }
     }
 
diff --git a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java 
b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java
new file mode 100644
index 0000000..bf59546
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+/**
+ * Cleans a direct {@link ByteBuffer}. Without manual intervention, direct 
ByteBuffers will be cleaned eventually upon
+ * garbage collection. However, this should not be be relied upon since it may 
not occur in a timely fashion -
+ * especially since off heap ByeBuffers don't put pressure on the garbage 
collector.
+ * <p>
+ * <b>Warning:</b> Do not attempt to use a direct {@link ByteBuffer} that has 
been cleaned or bad things will happen.
+ * Don't use this class unless you can ensure that the cleaned buffer will not 
be accessed anymore.
+ * </p>
+ * <p>
+ * See <a href=https://bugs.openjdk.java.net/browse/JDK-4724038>JDK-4724038</a>
+ * </p>
+ */
+class ByteBufferCleaner {
+
+    private interface Cleaner {
+        void clean(ByteBuffer buffer) throws ReflectiveOperationException;
+    }
+
+    private static class Java8Cleaner implements Cleaner {
+
+        private final Method cleanerMethod;
+        private final Method cleanMethod;
+
+        private Java8Cleaner() throws ReflectiveOperationException, 
SecurityException {
+            cleanMethod = Class.forName("sun.misc.Cleaner").getMethod("clean");
+            cleanerMethod = 
Class.forName("sun.nio.ch.DirectBuffer").getMethod("cleaner");
+        }
+
+        @Override
+        public void clean(final ByteBuffer buffer) throws 
ReflectiveOperationException {
+            final Object cleaner = cleanerMethod.invoke(buffer);
+            if (cleaner != null) {
+                cleanMethod.invoke(cleaner);
+            }
+        }
+    }
+
+    private static class Java9Cleaner implements Cleaner {
+
+        private final Object theUnsafe;
+        private final Method invokeCleaner;
+
+        private Java9Cleaner() throws ReflectiveOperationException, 
SecurityException {
+            final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
+            final Field field = unsafeClass.getDeclaredField("theUnsafe");
+            field.setAccessible(true);
+            theUnsafe = field.get(null);
+            invokeCleaner = unsafeClass.getMethod("invokeCleaner", 
ByteBuffer.class);
+        }
+
+        @Override
+        public void clean(final ByteBuffer buffer) throws 
ReflectiveOperationException {
+            invokeCleaner.invoke(theUnsafe, buffer);
+        }
+    }
+
+    private static final Cleaner INSTANCE = getCleaner();
+
+    /**
+     * Releases memory held by the given {@link ByteBuffer}.
+     *
+     * @param buffer to release.
+     * @throws IllegalStateException on internal failure.
+     */
+    static void clean(final ByteBuffer buffer) {
+        try {
+            INSTANCE.clean(buffer);
+        } catch (final Exception e) {
+            throw new IllegalStateException("Failed to clean direct buffer.", 
e);
+        }
+    }
+
+    private static Cleaner getCleaner() {
+        try {
+            return new Java8Cleaner();
+        } catch (final Exception e) {
+            try {
+                return new Java9Cleaner();
+            } catch (final Exception e1) {
+                throw new IllegalStateException("Failed to initialize a 
Cleaner.", e);
+            }
+        }
+    }
+
+    /**
+     * Tests if were able to load a suitable cleaner for the current JVM. 
Attempting to call
+     * {@code ByteBufferCleaner#clean(ByteBuffer)} when this method returns 
false will result in an exception.
+     *
+     * @return {@code true} if cleaning is supported, {@code false} otherwise.
+     */
+    static boolean isSupported() {
+        return INSTANCE != null;
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java 
b/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java
new file mode 100644
index 0000000..7be5c6a
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import static org.apache.commons.io.IOUtils.EOF;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+/**
+ * An {@link InputStream} that utilizes memory mapped files to improve 
performance. A sliding window of the file is
+ * mapped to memory to avoid mapping the entire file to memory at one time. 
The size of the sliding buffer is
+ * configurable.
+ * <p>
+ * For most operating systems, mapping a file into memory is more expensive 
than reading or writing a few tens of
+ * kilobytes of data. From the standpoint of performance. it is generally only 
worth mapping relatively large files into
+ * memory.
+ * </p>
+ * <p>
+ * Note: Use of this class does not necessarily obviate the need to use a 
{@link BufferedInputStream}. Depending on the
+ * use case, the use of buffering may still further improve performance. For 
example:
+ * </p>
+ * <pre>
+ * new BufferedInputStream(new GzipInputStream(new 
MemoryMappedFileInputStream(path))))
+ * </pre>
+ * <p>
+ * should outperform:
+ * </p>
+ * <pre>
+ * new GzipInputStream(new MemoryMappedFileInputStream(path))
+ * </pre>
+ *
+ * @since 2.12.0
+ */
+public class MemoryMappedFileInputStream extends InputStream {
+
+    /**
+     * Default size of the sliding memory mapped buffer. We use 256K, equal to 
65536 pages (given a 4K page size).
+     * Increasing the value beyond the default size will generally not provide 
any increase in throughput.
+     */
+    private static final int DEFAULT_BUFFER_SIZE = 256 * 1024;
+    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.wrap(new 
byte[0]).asReadOnlyBuffer();
+    private final int bufferSize;
+    private final FileChannel channel;
+    private ByteBuffer buffer = EMPTY_BUFFER;
+    private boolean closed;
+
+    /**
+     * The starting position (within the file) of the next sliding buffer.
+     */
+    private long nextBufferPosition;
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param file The path of the file to to open.
+     * @throws IOException If an I/O error occurs
+     */
+    public MemoryMappedFileInputStream(final Path file) throws IOException {
+        this(file, DEFAULT_BUFFER_SIZE);
+    }
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param file The path of the file to to open.
+     * @param bufferSize Size of the sliding buffer.
+     * @throws IOException If an I/O error occurs.
+     */
+    public MemoryMappedFileInputStream(final Path file, final int bufferSize) 
throws IOException {
+        this.bufferSize = bufferSize;
+        this.channel = FileChannel.open(file, StandardOpenOption.READ);
+    }
+
+    @Override
+    public int available() throws IOException {
+        return buffer.remaining();
+    }
+
+    private void cleanBuffer() {
+        if (ByteBufferCleaner.isSupported() && buffer.isDirect()) {
+            ByteBufferCleaner.clean(buffer);
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (!closed) {
+            cleanBuffer();
+            buffer = null;
+            channel.close();
+            closed = true;
+        }
+    }
+
+    private void ensureOpen() throws IOException {
+        if (closed) {
+            throw new IOException("Stream closed");
+        }
+    }
+
+    private void nextBuffer() throws IOException {
+        final long remainingInFile = channel.size() - nextBufferPosition;
+        if (remainingInFile > 0) {
+            final long amountToMap = Math.min(remainingInFile, bufferSize);
+            cleanBuffer();
+            buffer = channel.map(MapMode.READ_ONLY, nextBufferPosition, 
amountToMap);
+            nextBufferPosition += amountToMap;
+        } else {
+            buffer = EMPTY_BUFFER;
+        }
+    }
+
+    @Override
+    public int read() throws IOException {
+        ensureOpen();
+        if (!buffer.hasRemaining()) {
+            nextBuffer();
+            if (!buffer.hasRemaining()) {
+                return EOF;
+            }
+        }
+        return Short.toUnsignedInt(buffer.get());
+    }
+
+    @Override
+    public int read(final byte[] b, final int off, final int len) throws 
IOException {
+        ensureOpen();
+        if (!buffer.hasRemaining()) {
+            nextBuffer();
+            if (!buffer.hasRemaining()) {
+                return EOF;
+            }
+        }
+        final int numBytes = Math.min(buffer.remaining(), len);
+        buffer.get(b, off, numBytes);
+        return numBytes;
+    }
+
+    @Override
+    public long skip(final long n) throws IOException {
+        ensureOpen();
+        if (n <= 0) {
+            return 0;
+        }
+        if (n <= buffer.remaining()) {
+            buffer.position((int) (buffer.position() + n));
+            return n;
+        }
+        final long remainingInFile = channel.size() - nextBufferPosition;
+        final long skipped = buffer.remaining() + Math.min(remainingInFile, n 
- buffer.remaining());
+        nextBufferPosition += skipped - buffer.remaining();
+        nextBuffer();
+        return skipped;
+    }
+
+}
diff --git 
a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java 
b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java
new file mode 100644
index 0000000..be639a5
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.ByteBuffer;
+
+import org.apache.commons.lang3.RandomUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@code ByteBufferCleaner}.
+ */
+public class ByteBufferCleanerTest {
+
+    @Test
+    void testCleanEmpty() {
+       ByteBuffer buffer = ByteBuffer.allocateDirect(10);
+       // There is no way verify that the buffer has been cleaned up, we are 
just verifying that
+       // clean() doesn't blow up
+       ByteBufferCleaner.clean(buffer);
+    }
+    
+    @Test
+    void testCleanFull() {
+       ByteBuffer buffer = ByteBuffer.allocateDirect(10);
+       buffer.put(RandomUtils.nextBytes(10), 0, 10);
+       // There is no way verify that the buffer has been cleaned up, we are 
just verifying that
+       // clean() doesn't blow up
+       ByteBufferCleaner.clean(buffer);
+    }
+
+    @Test
+    void testSupported() {
+        assertTrue(ByteBufferCleaner.isSupported(), "ByteBufferCleaner does 
not work on this platform, please investigate and fix");
+    }
+
+}
diff --git 
a/src/test/java/org/apache/commons/io/input/MemoryMappedFileInputStreamTest.java
 
b/src/test/java/org/apache/commons/io/input/MemoryMappedFileInputStreamTest.java
new file mode 100644
index 0000000..2c23c1b
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/io/input/MemoryMappedFileInputStreamTest.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+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.ValueSource;
+
+/**
+ * Tests {@link MemoryMappedFileInputStream}.
+ */
+public class MemoryMappedFileInputStreamTest {
+
+    @TempDir
+    Path tempDir;
+
+    @AfterEach
+    void cleanup() {
+        // Ask to run the garbage collector to clean up memory mapped buffers,
+        // otherwise the temporary files won't be able to be removed when 
running on
+        // Windows. Calling gc() is just a hint to the VM.
+        System.gc();
+    }
+
+    private Path createTestFile(final int size) throws IOException {
+        final Path file = Files.createTempFile(tempDir, null, null);
+        try (OutputStream outputStream = new 
BufferedOutputStream(Files.newOutputStream(file))) {
+            Files.write(file, RandomUtils.nextBytes(size));
+        }
+        return file;
+    }
+
+    @Test
+    void testAlternateBufferSize() throws IOException {
+        // setup
+        final Path file = createTestFile(1024 * 1024);
+        final byte[] expectedData = Files.readAllBytes(file);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file, 
1024)) {
+            // verify
+            assertArrayEquals(expectedData, IOUtils.toByteArray(inputStream));
+        }
+    }
+
+    @Test
+    void testEmptyFile() throws IOException {
+        // setup
+        final Path file = createTestFile(0);
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file)) {
+            // verify
+            assertArrayEquals(new byte[0], IOUtils.toByteArray(inputStream));
+        }
+    }
+
+    @Test
+    void testLargerFile() throws IOException {
+        // setup
+        final Path file = createTestFile(1024 * 1024);
+        final byte[] expectedData = Files.readAllBytes(file);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file)) {
+            // verify
+            assertArrayEquals(expectedData, IOUtils.toByteArray(inputStream));
+        }
+    }
+
+    @Test
+    void testReadAfterClose() throws IOException {
+        // setup
+        final Path file = createTestFile(1 * 1024 * 1024);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file, 
1024)) {
+            inputStream.close();
+            // verify
+            Assertions.assertThrows(IOException.class, () -> 
IOUtils.toByteArray(inputStream));
+        }
+    }
+
+    @Test
+    void testReadSingleByte() throws IOException {
+        // setup
+        final Path file = createTestFile(2);
+        final byte[] expectedData = Files.readAllBytes(file);
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file, 
1024)) {
+            final int b1 = inputStream.read();
+            final int b2 = inputStream.read();
+            assertEquals(-1, inputStream.read());
+            // verify
+            assertArrayEquals(expectedData, new byte[] {(byte) b1, (byte) b2});
+        }
+    }
+
+    @Test
+    void testSkipAtStart() throws IOException {
+        // setup
+        final Path file = createTestFile(100);
+        final byte[] expectedData = Files.readAllBytes(file);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file, 
10)) {
+            assertEquals(1, inputStream.skip(1));
+            final byte[] data = IOUtils.toByteArray(inputStream);
+            // verify
+            assertArrayEquals(Arrays.copyOfRange(expectedData, 1, 
expectedData.length), data);
+        }
+    }
+
+    @Test
+    void testSkipEmpty() throws IOException {
+        // setup
+        final Path file = createTestFile(0);
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file)) {
+            assertEquals(0, inputStream.skip(5));
+            // verify
+            assertArrayEquals(new byte[0], IOUtils.toByteArray(inputStream));
+        }
+    }
+
+    @Test
+    void testSkipInCurrentBuffer() throws IOException {
+        // setup
+        final Path file = createTestFile(100);
+        final byte[] expectedData = Files.readAllBytes(file);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file, 
10)) {
+            IOUtils.toByteArray(inputStream, 5);
+            assertEquals(3, inputStream.skip(3));
+            final byte[] data = IOUtils.toByteArray(inputStream);
+            // verify
+            assertArrayEquals(Arrays.copyOfRange(expectedData, 8, 
expectedData.length), data);
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {-5, -1, 0})
+    void testSkipNoop(final int amountToSkip) throws IOException {
+        // setup
+        final Path file = createTestFile(10);
+        final byte[] expectedData = Files.readAllBytes(file);
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file)) {
+            assertEquals(0, inputStream.skip(amountToSkip));
+            // verify
+            assertArrayEquals(expectedData, IOUtils.toByteArray(inputStream));
+        }
+    }
+
+    @Test
+    void testSkipOutOfCurrentBuffer() throws IOException {
+        // setup
+        final Path file = createTestFile(100);
+        final byte[] expectedData = Files.readAllBytes(file);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file, 
10)) {
+            IOUtils.toByteArray(inputStream, 5);
+            assertEquals(6, inputStream.skip(6));
+            final byte[] data = IOUtils.toByteArray(inputStream);
+            // verify
+            assertArrayEquals(Arrays.copyOfRange(expectedData, 11, 
expectedData.length), data);
+        }
+    }
+
+    @Test
+    void testSkipPastEof() throws IOException {
+        // setup
+        final Path file = createTestFile(100);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file, 
10)) {
+            IOUtils.toByteArray(inputStream, 5);
+            assertEquals(95, inputStream.skip(96));
+            // verify
+            assertArrayEquals(new byte[0], IOUtils.toByteArray(inputStream));
+        }
+    }
+
+    @Test
+    void testSkipToEndOfCurrentBuffer() throws IOException {
+        // setup
+        final Path file = createTestFile(100);
+        final byte[] expectedData = Files.readAllBytes(file);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file, 
10)) {
+            IOUtils.toByteArray(inputStream, 5);
+            assertEquals(5, inputStream.skip(5));
+            final byte[] data = IOUtils.toByteArray(inputStream);
+            // verify
+            assertArrayEquals(Arrays.copyOfRange(expectedData, 10, 
expectedData.length), data);
+        }
+    }
+
+    @Test
+    void testSmallFile() throws IOException {
+        // setup
+        final Path file = createTestFile(100);
+        final byte[] expectedData = Files.readAllBytes(file);
+
+        // test
+        try (InputStream inputStream = new MemoryMappedFileInputStream(file)) {
+            // verify
+            assertArrayEquals(expectedData, IOUtils.toByteArray(inputStream));
+        }
+    }
+
+}

Reply via email to