Add UnsynchronizedFilterInputStream
MODIFY
src/changes/changes.xml
src/changes/changes.xml
@@ -457,6 +457,9 @@ The <action> type attribute can be add,update,fix,remove.
<action dev="ggregory" type="add" due-to="DaGeRe, Gary Gregory">
Add and use ThreadUtils.
</action>
+ <action issue="IO-786" dev="ggregory" type="add" due-to="Gary Gregory">
+ Add UnsynchronizedFilterInputStream.
+ </action>
<!-- UPDATE -->
<action dev="kinow" type="update" due-to="Dependabot, Gary Gregory">
Bump actions/cache from 2.1.6 to 3.0.10 #307, #337, #393.

ADD
src/main/java/org/apache/commons/io/input/UnsynchronizedFilterInputStream.java
+/*
+ * 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.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An unsynchronized version of {@link FilterInputStream}, not thread-safe.
+ * <p>
+ * Wraps an existing {@link InputStream} and performs some transformation on the input data while it is being read. Transformations can be anything from a
+ * simple byte-wise filtering input data to an on-the-fly compression or decompression of the underlying stream. Input streams that wrap another input stream
+ * and provide some additional functionality on top of it usually inherit from this class.
+ * </p>
+ * <p>
+ * Provenance: Apache Harmony and modified.
+ * </p>
+ *
+ * @see FilterInputStream
+ * @since 2.12.0
+ */
+//@NotThreadSafe
+public class UnsynchronizedFilterInputStream extends InputStream {
+
+ /**
+ * The source input stream that is filtered.
+ */
+ protected volatile InputStream in;
+
+ /**
+ * Constructs a new {@code FilterInputStream} with the specified input stream as source.
+ *
+ * @param in the non-null InputStream to filter reads on.
+ */
+ protected UnsynchronizedFilterInputStream(final InputStream in) {
+ this.in = in;
+ }
+
+ /**
+ * Returns the number of bytes that are available before this stream will block.
+ *
+ * @return the number of bytes available before blocking.
+ * @throws IOException if an error occurs in this stream.
+ */
+ @Override
+ public int available() throws IOException {
+ return in.available();
+ }
+
+ /**
+ * Closes this stream. This implementation closes the filtered stream.
+ *
+ * @throws IOException if an error occurs while closing this stream.
+ */
+ @Override
+ public void close() throws IOException {
+ in.close();
+ }
+
+ /**
+ * Sets a mark position in this stream. The parameter {@code readlimit} indicates how many bytes can be read before the mark is invalidated. Sending
+ * {@code reset()} will reposition this stream back to the marked position, provided that {@code readlimit} has not been surpassed.
+ * <p>
+ * This implementation sets a mark in the filtered stream.
+ *
+ * @param readlimit the number of bytes that can be read from this stream before the mark is invalidated.
+ * @see #markSupported()
+ * @see #reset()
+ */
+ @SuppressWarnings("sync-override") // by design.
+ @Override
+ public void mark(final int readlimit) {
+ in.mark(readlimit);
+ }
+
+ /**
+ * Indicates whether this stream supports {@code mark()} and {@code reset()}. This implementation returns whether or not the filtered stream supports
+ * marking.
+ *
+ * @return {@code true} if {@code mark()} and {@code reset()} are supported, {@code false} otherwise.
+ * @see #mark(int)
+ * @see #reset()
+ * @see #skip(long)
+ */
+ @Override
+ public boolean markSupported() {
+ return in.markSupported();
+ }
+
+ /**
+ * Reads a single byte from the filtered stream and returns it as an integer in the range from 0 to 255. Returns -1 if the end of this stream has been
+ * reached.
+ *
+ * @return the byte read or -1 if the end of the filtered stream has been reached.
+ * @throws IOException if the stream is closed or another IOException occurs.
+ */
+ @Override
+ public int read() throws IOException {
+ return in.read();
+ }
+
+ /**
+ * Reads bytes from this stream and stores them in the byte array {@code buffer}. Returns the number of bytes actually read or -1 if no bytes were read and
+ * the end of this stream was encountered. This implementation reads bytes from the filtered stream.
+ *
+ * @param buffer the byte array in which to store the read bytes.
+ * @return the number of bytes actually read or -1 if the end of the filtered stream has been reached while reading.
+ * @throws IOException if this stream is closed or another IOException occurs.
+ */
+ @Override
+ public int read(final byte[] buffer) throws IOException {
+ return read(buffer, 0, buffer.length);
+ }
+
+ /**
+ * Reads at most {@code count} bytes from this stream and stores them in the byte array {@code buffer} starting at {@code offset}. Returns the number of
+ * bytes actually read or -1 if no bytes have been read and the end of this stream has been reached. This implementation reads bytes from the filtered
+ * stream.
+ *
+ * @param buffer the byte array in which to store the bytes read.
+ * @param offset the initial position in {@code buffer} to store the bytes read from this stream.
+ * @param count the maximum number of bytes to store in {@code buffer}.
+ * @return the number of bytes actually read or -1 if the end of the filtered stream has been reached while reading.
+ * @throws IOException if this stream is closed or another I/O error occurs.
+ */
+ @Override
+ public int read(final byte[] buffer, final int offset, final int count) throws IOException {
+ return in.read(buffer, offset, count);
+ }
+
+ /**
+ * Resets this stream to the last marked location. This implementation resets the target stream.
+ *
+ * @throws IOException if this stream is already closed, no mark has been set or the mark is no longer valid because more than {@code readlimit} bytes have
+ * been read since setting the mark.
+ * @see #mark(int)
+ * @see #markSupported()
+ */
+ @SuppressWarnings("sync-override") // by design.
+ @Override
+ public void reset() throws IOException {
+ in.reset();
+ }
+
+ /**
+ * Skips {@code count} number of bytes in this stream. Subsequent {@code read()}'s will not return these bytes unless {@code reset()} is used. This
+ * implementation skips {@code count} number of bytes in the filtered stream.
+ *
+ * @param count the number of bytes to skip.
+ * @return the number of bytes actually skipped.
+ * @throws IOException if this stream is closed or another IOException occurs.
+ * @see #mark(int)
+ * @see #reset()
+ */
+ @Override
+ public long skip(final long count) throws IOException {
+ return in.skip(count);
+ }
+}

ADD
src/test/java/org/apache/commons/io/input/UnsynchronizedFilterInputStreamTest.java
+package org.apache.commons.io.input;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link UnsynchronizedFilterInputStream}.
+ * <p>
+ * Provenance: Apache Harmony and modified.
+ * </p>
+ */
+public class UnsynchronizedFilterInputStreamTest {
+
+ static class MyUnsynchronizedFilterInputStream extends UnsynchronizedFilterInputStream {
+ public MyUnsynchronizedFilterInputStream(final InputStream is) {
+ super(is);
+ }
+ }
+
+ private Path fileName;
+
+ private InputStream is;
+
+ byte[] ibuf = new byte[4096];
+
+ public static final String DATA = StringUtils.repeat("This is a test.", 500);
+
+ /**
+ * Sets up the fixture, for example, open a network connection. This method is called before a test is executed.
+ *
+ * @throws IOException Thrown on test failure.
+ */
+ @SuppressWarnings("resource") // See @AfterEach tearDown() method
+ @BeforeEach
+ protected void setUp() throws IOException {
+ fileName = Files.createTempFile(getClass().getSimpleName(), ".tst");
+ Files.write(fileName, DATA.getBytes("UTF-8"));
+ is = new MyUnsynchronizedFilterInputStream(Files.newInputStream(fileName));
+ }
+
+ /**
+ * Tears down the fixture, for example, close a network connection. This method is called after a test is executed.
+ *
+ * @throws IOException Thrown on test failure.
+ */
+ @AfterEach
+ protected void tearDown() throws IOException {
+ IOUtils.closeQuietly(is);
+ Files.deleteIfExists(fileName);
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#available()
+ *
+ * @throws IOException Thrown on test failure.
+ */
+ @Test
+ public void test_available() throws IOException {
+ assertTrue(is.available() == DATA.length(), "Returned incorrect number of available bytes");
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#close()
+ *
+ * @throws IOException Thrown on test failure.
+ */
+ @Test
+ public void test_close() throws IOException {
+ is.close();
+ assertThrows(IOException.class, () -> is.read(), "Able to read from closed stream");
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#mark(int)
+ */
+ @Test
+ public void test_markI() {
+ assertTrue(true, "Mark not supported by parent InputStream");
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#markSupported()
+ */
+ @Test
+ public void test_markSupported() {
+ assertTrue(!is.markSupported(), "markSupported returned true");
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#read()
+ *
+ * @throws IOException Thrown on test failure.
+ */
+ @Test
+ public void test_read() throws IOException {
+ final int c = is.read();
+ assertTrue(c == DATA.charAt(0), "read returned incorrect char");
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#read(byte[])
+ *
+ * @throws IOException Thrown on test failure.
+ */
+ @Test
+ public void test_read$B() throws IOException {
+ final byte[] buf1 = new byte[100];
+ is.read(buf1);
+ assertTrue(new String(buf1, 0, buf1.length, "UTF-8").equals(DATA.substring(0, 100)), "Failed to read correct data");
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#read(byte[], int, int)
+ *
+ * @throws IOException Thrown on test failure.
+ */
+ @Test
+ public void test_read$BII() throws IOException {
+ final byte[] buf1 = new byte[100];
+ is.skip(3000);
+ is.mark(1000);
+ is.read(buf1, 0, buf1.length);
+ assertTrue(new String(buf1, 0, buf1.length, "UTF-8").equals(DATA.substring(3000, 3100)), "Failed to read correct data");
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#reset()
+ */
+ @Test
+ public void test_reset() {
+ assertThrows(IOException.class, () -> is.reset(), "should throw IOException");
+
+ }
+
+ /**
+ * Tests java.io.FilterInputStream#skip(long)
+ *
+ * @throws IOException Thrown on test failure.
+ */
+ @Test
+ public void test_skipJ() throws IOException {
+ final byte[] buf1 = new byte[10];
+ is.skip(1000);
+ is.read(buf1, 0, buf1.length);
+ assertTrue(new String(buf1, 0, buf1.length, "UTF-8").equals(DATA.substring(1000, 1010)), "Failed to skip to correct position");
+ }
+}

[IO-786] Add UnsynchronizedFilterInputStream