diff options
author | Gary Gregory <garydgregory@gmail.com> | 2023-02-01 09:10:51 -0500 |
---|---|---|
committer | Gary Gregory <garydgregory@gmail.com> | 2023-02-01 09:10:51 -0500 |
commit | 1e7b2925a24e99d6cf092b5adae136bf055a001b (patch) | |
tree | 54ca8e1acf3d0cf282b8cf8387b435b4fc252578 | |
parent | 7323c8b3e3a4e6a9f2cd26ca63724290ce25de5e (diff) | |
download | apache-commons-io-1e7b2925a24e99d6cf092b5adae136bf055a001b.tar.gz |
[IO-786] Add UnsynchronizedFilterInputStream
3 files changed, 353 insertions, 0 deletions
diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 966d463a..412f94ab 100644 --- a/src/changes/changes.xml +++ b/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. diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedFilterInputStream.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedFilterInputStream.java new file mode 100644 index 00000000..b76c668e --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedFilterInputStream.java @@ -0,0 +1,175 @@ +/* + * 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); + } +} diff --git a/src/test/java/org/apache/commons/io/input/UnsynchronizedFilterInputStreamTest.java b/src/test/java/org/apache/commons/io/input/UnsynchronizedFilterInputStreamTest.java new file mode 100644 index 00000000..005507d8 --- /dev/null +++ b/src/test/java/org/apache/commons/io/input/UnsynchronizedFilterInputStreamTest.java @@ -0,0 +1,175 @@ +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"); + } +} |