SequenceReader should close readers when its close method is
called
MODIFY
src/changes/changes.xml
src/changes/changes.xml
@@ -210,6 +210,9 @@ The <action> type attribute can be add,update,fix,remove.
<action issue="IO-611" dev="ggregory" type="fix" due-to="ArdenL-Liu, Bruno P. Kinoshita, Gary Gregory">
Better docs in IOUtils and IOUtils.byteArray(int size) #374.
</action>
+ <action issue="IO-782" dev="ggregory" type="fix" due-to="Matteo Di Giovinazzo, Gary Gregory">
+ SequenceReader should close readers when its close method is called #391.
+ </action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary Gregory">
Add GitHub coverage.yml.

MODIFY
src/main/java/org/apache/commons/io/input/SequenceReader.java
src/main/java/org/apache/commons/io/input/SequenceReader.java
import java.io.IOException;
import java.io.Reader;
+import java.io.SequenceInputStream;
+import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
/**
- * Provides the contents of multiple Readers in sequence.
+ * Provides the contents of multiple {@link Reader}s in sequence.
+ * <p>
+ * Like {@link SequenceInputStream} but for {@link Reader} arguments.
+ * </p>
*
* @since 2.7
*/
public class SequenceReader extends Reader {
private Reader reader;
- private Iterator<? extends Reader> readers;
+ private final Iterator<? extends Reader> readers;
/**
* Constructs a new instance with readers
*/
public SequenceReader(final Iterable<? extends Reader> readers) {
this.readers = Objects.requireNonNull(readers, "readers").iterator();
+ try {
this.reader = nextReader();
+ } catch (final IOException e) {
+ throw new UncheckedIOException(e);
+ }
}
/**
@@ -60,17 +69,27 @@ public SequenceReader(final Reader... readers) {
*/
@Override
public void close() throws IOException {
- this.readers = null;
- this.reader = null;
+ do { // NOPMD
+ // empty
+ } while (nextReader() != null);
}
/**
* Returns the next available reader or null if done.
*
- * @return the next available reader or null
+ * @return the next available reader or null.
+ * @throws IOException IOException If an I/O error occurs.
*/
- private Reader nextReader() {
- return this.readers.hasNext() ? this.readers.next() : null;
+ private Reader nextReader() throws IOException {
+ if (reader != null) {
+ reader.close();
+ }
+ if (readers.hasNext()) {
+ reader = readers.next();
+ } else {
+ reader = null;
+ }
+ return reader;
}
/*
@@ -86,7 +105,7 @@ public int read() throws IOException {
if (c != EOF) {
break;
}
- reader = nextReader();
+ nextReader();
}
return c;
}
@@ -106,7 +125,7 @@ public int read(final char[] cbuf, int off, int len) throws IOException {
while (reader != null) {
final int readLen = reader.read(cbuf, off, len);
if (readLen == EOF) {
- reader = nextReader();
+ nextReader();
} else {
count += readLen;
off += readLen;

MODIFY
src/test/java/org/apache/commons/io/input/SequenceReaderTest.java
src/test/java/org/apache/commons/io/input/SequenceReaderTest.java
*/
package org.apache.commons.io.input;
+import static org.apache.commons.io.IOUtils.EOF;
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 static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
-
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;
*/
public class SequenceReaderTest {
+ private static class CustomReader extends Reader {
+
+ boolean closed;
+
+ protected void checkOpen() throws IOException {
+ if (closed) {
+ throw new IOException("emptyReader already closed");
+ }
+ }
+
+ @Override
+ public int read(final char[] cbuf, final int off, final int len) throws IOException {
+ checkOpen();
+ close();
+ return EOF;
+ }
+
+ @Override
+ public void close() throws IOException {
+ closed = true;
+ }
+
+ public boolean isClosed() {
+ return closed;
+ }
+ };
+
private static final char NUL = 0;
private void checkArray(final char[] expected, final char[] actual) {
@@ -56,7 +84,7 @@ private void checkReadEof(final Reader reader) throws IOException {
}
@Test
- public void testClose() throws IOException {
+ public void testAutoClose() throws IOException {
try (Reader reader = new SequenceReader(new CharSequenceReader("FooBar"))) {
checkRead(reader, "Foo");
reader.close();
@@ -64,6 +92,57 @@ public void testClose() throws IOException {
}
}
+ @Test
+ public void testClose() throws IOException {
+ final Reader reader = new SequenceReader(new CharSequenceReader("FooBar"));
+ checkRead(reader, "Foo");
+ reader.close();
+ checkReadEof(reader);
+ }
+
+ @Test
+ public void testCloseReaders() throws IOException {
+ final CustomReader reader0 = new CustomReader();
+ final CustomReader reader1 = new CustomReader() {
+
+ private final char[] content = {'A'};
+ private int position;
+
+ @Override
+ public int read(final char[] cbuf, final int off, final int len) throws IOException {
+ checkOpen();
+
+ if (off < 0) {
+ throw new IndexOutOfBoundsException("off is negative");
+ } else if (len < 0) {
+ throw new IndexOutOfBoundsException("len is negative");
+ } else if (len > cbuf.length - off) {
+ throw new IndexOutOfBoundsException("len is greater than cbuf.length - off");
+ }
+
+ if (position > 0) {
+ return EOF;
+ }
+
+ cbuf[off] = content[0];
+ position++;
+ return 1;
+ }
+
+ };
+
+ try (SequenceReader sequenceReader = new SequenceReader(reader1, reader0)) {
+ assertEquals('A', sequenceReader.read());
+ assertEquals(EOF, sequenceReader.read());
+ } finally {
+ assertTrue(reader1.isClosed());
+ assertTrue(reader0.isClosed());
+ }
+ assertTrue(reader1.isClosed());
+ assertTrue(reader0.isClosed());
+
+ }
+
@Test
public void testMarkSupported() throws Exception {
try (Reader reader = new SequenceReader()) {

[IO-782] SequenceReader should close readers when its close method is
called #391.