Add a SequenceReader similar to java.io.SequenceInputStream.
This new class is based on Joshua Gitlin's PR
but redone to cover a much
broader range of types through an Iterable ctor instead of a List.
Closes
MODIFY
checkstyle.xml
checkstyle.xml
@@ -31,7 +31,6 @@ limitations under the License.
<property name="fileExtensions" value="java,xml"/>
</module>
<module name="TreeWalker">
- <property name="cacheFile" value="target/cachefile"/>
<module name="AvoidStarImport"/>
<module name="RedundantImport"/>
<module name="UnusedImports"/>

MODIFY
pom.xml
pom.xml
@@ -286,7 +286,7 @@ file comparators, endian transformation classes, and much more.
</commons.osgi.export>
<commons.scmPubUrl>https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-io/</commons.scmPubUrl>
<commons.scmPubCheckoutDirectory>site-content</commons.scmPubCheckoutDirectory>
- <checkstyle.plugin.version>3.0.0</checkstyle.plugin.version>
+ <checkstyle.plugin.version>3.1.0</checkstyle.plugin.version>
<commons.jacoco.version>0.8.4</commons.jacoco.version>
<commons.surefire.version>2.22.2</commons.surefire.version>
<commons.japicmp.version>0.14.1</commons.japicmp.version>

MODIFY
src/changes/changes.xml
src/changes/changes.xml
@@ -180,6 +180,9 @@ The <action> type attribute can be add,update,fix,remove.
<action issue="IO-645" dev="ggregory" type="add" due-to="Gary Gregory">
Add org.apache.commons.io.file.PathUtils.fileContentEquals(Path, Path, OpenOption...).
</action>
+ <action issue="IO-458" dev="ggregory" type="add" due-to="Gary Gregory, Joshua Gitlin">
+ Add a SequenceReader similar to java.io.SequenceInputStream.
+ </action>
</release>
<release version="2.6" date="2017-10-15" description="Java 7 required, Java 9 supported.">

ADD
src/main/java/org/apache/commons/io/input/SequenceReader.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 static org.apache.commons.io.IOUtils.EOF;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * Provides the contents of multiple Readers in sequence.
+ *
+ * @since 2.7
+ */
+public class SequenceReader extends Reader {
+
+ private Reader reader;
+ private Iterator<? extends Reader> readers;
+
+ /**
+ * Construct a new instance with readers
+ *
+ * @param readers the readers to read
+ */
+ public SequenceReader(final Iterable<? extends Reader> readers) {
+ this.readers = Objects.requireNonNull(readers, "readers").iterator();
+ this.reader = this.readers.hasNext() ? this.readers.next() : null;
+ }
+
+ /**
+ * Construct a new instance with readers
+ *
+ * @param readers the readers to read
+ */
+ public SequenceReader(final Reader... readers) {
+ this(Arrays.asList(readers));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.io.Reader#close()
+ */
+ @Override
+ public void close() throws IOException {
+ this.readers = null;
+ this.reader = null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.io.Reader#read(char[], int, int)
+ */
+ @Override
+ public int read() throws IOException {
+ int c = EOF;
+ while (reader != null) {
+ c = reader.read();
+ if (c == EOF) {
+ reader = readers.hasNext() ? readers.next() : null;
+ } else {
+ break;
+ }
+ }
+ return c;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.io.Reader#read()
+ */
+ @Override
+ public int read(final char[] cbuf, int off, int len) throws IOException {
+ Objects.requireNonNull(cbuf, "cbuf");
+ if (len < 0 || off < 0 || off + len > cbuf.length) {
+ throw new IndexOutOfBoundsException("Array Size=" + cbuf.length + ", offset=" + off + ", length=" + len);
+ }
+ int count = 0;
+ while (reader != null) {
+ final int readLen = reader.read(cbuf, off, len);
+ if (readLen == EOF) {
+ reader = readers.hasNext() ? readers.next() : null;
+ } else {
+ count += readLen;
+ off += readLen;
+ len -= readLen;
+ if (len <= 0) {
+ break;
+ }
+ }
+ }
+ if (count > 0) {
+ return count;
+ }
+ return EOF;
+ }
+}

ADD
src/test/java/org/apache/commons/io/input/SequenceReaderTest.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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test case for {@link SequenceReader}.
+ */
+public class SequenceReaderTest {
+
+ private static final char NUL = 0;
+
+ private void checkArray(final char[] expected, final char[] actual) {
+ for (int i = 0; i < expected.length; i++) {
+ assertEquals(expected[i], actual[i], "Compare[" + i + "]");
+ }
+ }
+
+ private void checkRead(final Reader reader, final String expected) throws IOException {
+ for (int i = 0; i < expected.length(); i++) {
+ assertEquals(expected.charAt(i), (char) reader.read(), "Read[" + i + "] of '" + expected + "'");
+ }
+ }
+
+ private void checkReadEof(final Reader reader) throws IOException {
+ for (int i = 0; i < 10; i++) {
+ assertEquals(-1, reader.read());
+ }
+ }
+
+ @Test
+ public void testClose() throws IOException {
+ try (final Reader reader = new SequenceReader(new CharSequenceReader("FooBar"))) {
+ checkRead(reader, "Foo");
+ reader.close();
+ checkReadEof(reader);
+ }
+ }
+
+ @Test
+ public void testReadClosedReader() throws IOException {
+ @SuppressWarnings("resource")
+ final Reader reader = new SequenceReader(new CharSequenceReader("FooBar"));
+ reader.close();
+ checkReadEof(reader);
+ }
+
+ @Test
+ public void testMarkSupported() throws Exception {
+ try (final Reader reader = new SequenceReader()) {
+ assertFalse(reader.markSupported());
+ }
+ }
+
+ @Test
+ public void testRead() throws IOException {
+ try (final Reader reader = new SequenceReader(new StringReader("Foo"), new StringReader("Bar"))) {
+ assertEquals('F', reader.read());
+ assertEquals('o', reader.read());
+ assertEquals('o', reader.read());
+ assertEquals('B', reader.read());
+ assertEquals('a', reader.read());
+ assertEquals('r', reader.read());
+ checkReadEof(reader);
+ }
+ }
+
+ @Test
+ public void testReadCharArray() throws IOException {
+ try (final Reader reader = new SequenceReader(new StringReader("Foo"), new StringReader("Bar"))) {
+ char[] chars = new char[2];
+ assertEquals(2, reader.read(chars));
+ checkArray(new char[] { 'F', 'o' }, chars);
+ chars = new char[3];
+ assertEquals(3, reader.read(chars));
+ checkArray(new char[] { 'o', 'B', 'a' }, chars);
+ chars = new char[3];
+ assertEquals(1, reader.read(chars));
+ checkArray(new char[] { 'r', NUL, NUL }, chars);
+ assertEquals(-1, reader.read(chars));
+ }
+ }
+
+ @Test
+ public void testReadCharArrayPortion() throws IOException {
+ final char[] chars = new char[10];
+ try (final Reader reader = new SequenceReader(new StringReader("Foo"), new StringReader("Bar"))) {
+ assertEquals(3, reader.read(chars, 3, 3));
+ checkArray(new char[] { NUL, NUL, NUL, 'F', 'o', 'o' }, chars);
+ assertEquals(3, reader.read(chars, 0, 3));
+ checkArray(new char[] { 'B', 'a', 'r', 'F', 'o', 'o', NUL }, chars);
+ assertEquals(-1, reader.read(chars));
+ assertThrows(IndexOutOfBoundsException.class, () -> reader.read(chars, 10, 10));
+ assertThrows(NullPointerException.class, () -> reader.read(null, 0, 10));
+ }
+ }
+
+ @Test
+ public void testReadCollection() throws IOException {
+ Collection<Reader> readers = new ArrayList<>();
+ readers.add(new StringReader("F"));
+ readers.add(new StringReader("B"));
+ try (final Reader reader = new SequenceReader(readers)) {
+ assertEquals('F', reader.read());
+ assertEquals('B', reader.read());
+ checkReadEof(reader);
+ }
+ }
+
+ @Test
+ public void testReadIterable() throws IOException {
+ Collection<Reader> readers = new ArrayList<>();
+ readers.add(new StringReader("F"));
+ readers.add(new StringReader("B"));
+ Iterable<Reader> iterable = readers;
+ try (final Reader reader = new SequenceReader(iterable)) {
+ assertEquals('F', reader.read());
+ assertEquals('B', reader.read());
+ checkReadEof(reader);
+ }
+ }
+
+ @Test
+ public void testReadLength0Readers() throws IOException {
+ try (final Reader reader = new SequenceReader(new StringReader(StringUtils.EMPTY),
+ new StringReader(StringUtils.EMPTY), new StringReader(StringUtils.EMPTY))) {
+ checkReadEof(reader);
+ }
+ }
+
+ @Test
+ public void testReadLength1Readers() throws IOException {
+ try (final Reader reader = new SequenceReader(
+ // @formatter:off
+ new StringReader("0"),
+ new StringReader("1"),
+ new StringReader("2"),
+ new StringReader("3"))) {
+ // @formatter:on
+ assertEquals('0', reader.read());
+ assertEquals('1', reader.read());
+ assertEquals('2', reader.read());
+ assertEquals('3', reader.read());
+ }
+ }
+
+ @Test
+ public void testReadList() throws IOException {
+ List<Reader> readers = new ArrayList<>();
+ readers.add(new StringReader("F"));
+ readers.add(new StringReader("B"));
+ try (final Reader reader = new SequenceReader(readers)) {
+ assertEquals('F', reader.read());
+ assertEquals('B', reader.read());
+ checkReadEof(reader);
+ }
+ }
+
+ @Test
+ public void testSkip() throws IOException {
+ try (final Reader reader = new SequenceReader(new StringReader("Foo"), new StringReader("Bar"))) {
+ assertEquals(3, reader.skip(3));
+ checkRead(reader, "Bar");
+ assertEquals(0, reader.skip(3));
+ }
+ }
+}
\ No newline at end of file

[IO-458] Add a SequenceReader similar to java.io.SequenceInputStream.

This new class is based on Joshua Gitlin's PR
https://github.com/apache/commons-io/pull/99 but redone to cover a much
broader range of types through an Iterable ctor instead of a List.
Closes #99.