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
The following commit(s) were added to refs/heads/master by this push: new 1e37482 [IO-612] Add class TeeReader. PR from Rob Spoor but modified. 1e37482 is described below commit 1e374820d0251105a87b9ac86f0c8fa32ce8031b Author: Gary Gregory <gardgreg...@gmail.com> AuthorDate: Tue Aug 6 21:16:38 2019 -0400 [IO-612] Add class TeeReader. PR from Rob Spoor but modified. --- pom.xml | 6 + src/changes/changes.xml | 3 + .../apache/commons/io/input/NullInputStream.java | 10 ++ .../org/apache/commons/io/input/NullReader.java | 13 +- .../org/apache/commons/io/input/TeeReader.java | 168 +++++++++++++++++++ .../commons/io/input/TeeInputStreamTest.java | 57 +++++++ .../org/apache/commons/io/input/TeeReaderTest.java | 182 +++++++++++++++++++++ .../io/testtools/YellOnCloseInputStream.java | 15 +- ...putStream.java => YellOnCloseOutputStream.java} | 26 +-- ...loseInputStream.java => YellOnCloseReader.java} | 27 +-- ...loseInputStream.java => YellOnCloseWriter.java} | 24 ++- 11 files changed, 496 insertions(+), 35 deletions(-) diff --git a/pom.xml b/pom.xml index c63f2c1..b278282 100644 --- a/pom.xml +++ b/pom.xml @@ -233,6 +233,12 @@ file comparators, endian transformation classes, and much more. <scope>test</scope> </dependency> <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>3.0.0</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>com.google.jimfs</groupId> <artifactId>jimfs</artifactId> <version>1.1</version> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b8ac8f1..875ea25 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -110,6 +110,9 @@ The <action> type attribute can be add,update,fix,remove. <action issue="IO-610" dev="ggregory" type="update" due-to="Sebastian"> Remove throws IOException in method isSymlink() #80. </action> + <action issue="IO-612" dev="ggregory" type="add" due-to="Rob Spoor, Gary Gregory"> + Add class TeeReader. + </action> </release> <release version="2.6" date="2017-10-15" description="Java 7 required, Java 9 supported."> diff --git a/src/main/java/org/apache/commons/io/input/NullInputStream.java b/src/main/java/org/apache/commons/io/input/NullInputStream.java index ad8be20..2df3e66 100644 --- a/src/main/java/org/apache/commons/io/input/NullInputStream.java +++ b/src/main/java/org/apache/commons/io/input/NullInputStream.java @@ -73,6 +73,16 @@ public class NullInputStream extends InputStream { private final boolean markSupported; /** + * Create an {@link InputStream} that emulates a size 0 stream + * which supports marking and does not throw EOFException. + * + * @since 2.7 + */ + public NullInputStream() { + this(0, true, false); + } + + /** * Create an {@link InputStream} that emulates a specified size * which supports marking and does not throw EOFException. * diff --git a/src/main/java/org/apache/commons/io/input/NullReader.java b/src/main/java/org/apache/commons/io/input/NullReader.java index 6a24d25..b361e54 100644 --- a/src/main/java/org/apache/commons/io/input/NullReader.java +++ b/src/main/java/org/apache/commons/io/input/NullReader.java @@ -45,7 +45,7 @@ import java.io.Reader; * <code>processChars()</code> methods can be implemented to generate * data, for example: * </p> - * + * * <pre> * public class TestReader extends NullReader { * public TestReader(int size) { @@ -63,7 +63,6 @@ import java.io.Reader; * </pre> * * @since 1.3 - * */ public class NullReader extends Reader { @@ -76,6 +75,16 @@ public class NullReader extends Reader { private final boolean markSupported; /** + * Creates a {@link Reader} that emulates a size 0 reader + * which supports marking and does not throw EOFException. + * + * @since 2.7 + */ + public NullReader() { + this(0, true, false); + } + + /** * Creates a {@link Reader} that emulates a specified size * which supports marking and does not throw EOFException. * diff --git a/src/main/java/org/apache/commons/io/input/TeeReader.java b/src/main/java/org/apache/commons/io/input/TeeReader.java new file mode 100644 index 0000000..827f062 --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/TeeReader.java @@ -0,0 +1,168 @@ +/* + * 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.io.Writer; +import java.nio.CharBuffer; + +/** + * Reader proxy that transparently writes a copy of all characters read from the proxied reader to a given Reader. Using + * {@link #skip(long)} or {@link #mark(int)}/{@link #reset()} on the reader will result on some characters from the + * reader being skipped or duplicated in the writer. + * <p> + * The proxied reader is closed when the {@link #close()} method is called on this proxy. You may configure whether the + * reader closes the writer. + * </p> + * + * @since 2.7 + */ +public class TeeReader extends ProxyReader { + + /** + * The writer that will receive a copy of all characters read from the proxied reader. + */ + private final Writer branch; + + /** + * Flag for closing the associated writer when this reader is closed. + */ + private final boolean closeBranch; + + /** + * Creates a TeeReader that proxies the given {@link Reader} and copies all read characters to the given + * {@link Writer}. The given writer will not be closed when this reader gets closed. + * + * @param input reader to be proxied + * @param branch writer that will receive a copy of all characters read + */ + public TeeReader(final Reader input, final Writer branch) { + this(input, branch, false); + } + + /** + * Creates a TeeReader that proxies the given {@link Reader} and copies all read characters to the given + * {@link Writer}. The given writer will be closed when this reader gets closed if the closeBranch parameter is + * {@code true}. + * + * @param input reader to be proxied + * @param branch writer that will receive a copy of all characters read + * @param closeBranch flag for closing also the writer when this reader is closed + */ + public TeeReader(final Reader input, final Writer branch, final boolean closeBranch) { + super(input); + this.branch = branch; + this.closeBranch = closeBranch; + } + + /** + * Closes the proxied reader and, if so configured, the associated writer. An exception thrown from the reader will + * not prevent closing of the writer. + * + * @throws IOException if either the reader or writer could not be closed + */ + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + if (closeBranch) { + branch.close(); + } + } + } + + /** + * Reads a single chracter from the proxied reader and writes it to the associated writer. + * + * @return next character from the reader, or -1 if the reader has ended + * @throws IOException if the reader could not be read (or written) + */ + @Override + public int read() throws IOException { + final int ch = super.read(); + if (ch != EOF) { + branch.write(ch); + } + return ch; + } + + /** + * Reads characters from the proxied reader and writes the read characters to the associated writer. + * + * @param chr character buffer + * @return number of characters read, or -1 if the reader has ended + * @throws IOException if the reader could not be read (or written) + */ + @Override + public int read(final char[] chr) throws IOException { + final int n = super.read(chr); + if (n != EOF) { + branch.write(chr, 0, n); + } + return n; + } + + /** + * Reads characters from the proxied reader and writes the read characters to the associated writer. + * + * @param chr character buffer + * @param st start offset within the buffer + * @param end maximum number of characters to read + * @return number of characters read, or -1 if the reader has ended + * @throws IOException if the reader could not be read (or written) + */ + @Override + public int read(final char[] chr, final int st, final int end) throws IOException { + final int n = super.read(chr, st, end); + if (n != EOF) { + branch.write(chr, st, n); + } + return n; + } + + /** + * Reads characters from the proxied reader and writes the read characters to the associated writer. + * + * @param target character buffer + * @return number of characters read, or -1 if the reader has ended + * @throws IOException if the reader could not be read (or written) + */ + @Override + public int read(final CharBuffer target) throws IOException { + final int originalPosition = target.position(); + final int n = super.read(target); + if (n != EOF) { + // Appending can only be done after resetting the CharBuffer to the + // right position and limit. + final int newPosition = target.position(); + final int newLimit = target.limit(); + try { + target.position(originalPosition).limit(newPosition); + branch.append(target); + } finally { + // Reset the CharBuffer as if the appending never happened. + target.position(newPosition).limit(newLimit); + } + } + return n; + } + +} diff --git a/src/test/java/org/apache/commons/io/input/TeeInputStreamTest.java b/src/test/java/org/apache/commons/io/input/TeeInputStreamTest.java index c1df877..bfb4f3f 100644 --- a/src/test/java/org/apache/commons/io/input/TeeInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/TeeInputStreamTest.java @@ -17,11 +17,20 @@ package org.apache.commons.io.input; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import org.apache.commons.io.testtools.YellOnCloseInputStream; +import org.apache.commons.io.testtools.YellOnCloseOutputStream; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -106,4 +115,52 @@ public class TeeInputStreamTest { assertEquals("abbc", new String(output.toString(ASCII))); } + /** + * Tests that the main {@code InputStream} is closed when closing the branch {@code OutputStream} throws an + * exception on {@link TeeInputStream#close()}, if specified to do so. + */ + @Test + public void testCloseBranchIOException() throws Exception { + final ByteArrayInputStream goodIs = mock(ByteArrayInputStream.class); + final OutputStream badOs = new YellOnCloseOutputStream(); + + final TeeInputStream nonClosingTis = new TeeInputStream(goodIs, badOs, false); + nonClosingTis.close(); + verify(goodIs).close(); + + final TeeInputStream closingTis = new TeeInputStream(goodIs, badOs, true); + try { + closingTis.close(); + Assert.fail("Expected " + IOException.class.getName()); + } catch (final IOException e) { + verify(goodIs, times(2)).close(); + } + } + + /** + * Tests that the branch {@code OutputStream} is closed when closing the main {@code InputStream} throws an + * exception on {@link TeeInputStream#close()}, if specified to do so. + */ + @Test + public void testCloseMainIOException() throws IOException { + final InputStream badIs = new YellOnCloseInputStream(); + final ByteArrayOutputStream goodOs = mock(ByteArrayOutputStream.class); + + final TeeInputStream nonClosingTis = new TeeInputStream(badIs, goodOs, false); + try { + nonClosingTis.close(); + Assert.fail("Expected " + IOException.class.getName()); + } catch (final IOException e) { + verify(goodOs, never()).close(); + } + + final TeeInputStream closingTis = new TeeInputStream(badIs, goodOs, true); + try { + closingTis.close(); + Assert.fail("Expected " + IOException.class.getName()); + } catch (final IOException e) { + verify(goodOs).close(); + } + } + } diff --git a/src/test/java/org/apache/commons/io/input/TeeReaderTest.java b/src/test/java/org/apache/commons/io/input/TeeReaderTest.java new file mode 100644 index 0000000..de175bb --- /dev/null +++ b/src/test/java/org/apache/commons/io/input/TeeReaderTest.java @@ -0,0 +1,182 @@ +/* + * 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.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.CharBuffer; + +import org.apache.commons.io.output.StringBuilderWriter; +import org.apache.commons.io.testtools.YellOnCloseReader; +import org.apache.commons.io.testtools.YellOnCloseWriter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * JUnit Test Case for {@link TeeReader}. + */ +public class TeeReaderTest { + + private StringBuilderWriter output; + + private Reader tee; + + @Before + public void setUp() throws Exception { + final Reader input = new CharSequenceReader("abc"); + output = new StringBuilderWriter(); + tee = new TeeReader(input, output); + } + + /** + * Tests that the main {@code Reader} is closed when closing the branch {@code Writer} throws an + * exception on {@link TeeReader#close()}, if specified to do so. + */ + @Test + public void testCloseBranchIOException() throws Exception { + final StringReader goodR = mock(StringReader.class); + final Writer badW = new YellOnCloseWriter(); + + final TeeReader nonClosingTr = new TeeReader(goodR, badW, false); + nonClosingTr.close(); + verify(goodR).close(); + + final TeeReader closingTr = new TeeReader(goodR, badW, true); + try { + closingTr.close(); + Assert.fail("Expected " + IOException.class.getName()); + } catch (final IOException e) { + verify(goodR, times(2)).close(); + } + } + + /** + * Tests that the branch {@code Writer} is closed when closing the main {@code Reader} throws an + * exception on {@link TeeReader#close()}, if specified to do so. + */ + @Test + public void testCloseMainIOException() throws IOException { + final Reader badR = new YellOnCloseReader(); + final StringWriter goodW = mock(StringWriter.class); + + final TeeReader nonClosingTr = new TeeReader(badR, goodW, false); + try { + nonClosingTr.close(); + Assert.fail("Expected " + IOException.class.getName()); + } catch (final IOException e) { + verify(goodW, never()).close(); + } + + final TeeReader closingTr = new TeeReader(badR, goodW, true); + try { + closingTr.close(); + Assert.fail("Expected " + IOException.class.getName()); + } catch (final IOException e) { + //Assert.assertTrue(goodW.closed); + verify(goodW).close(); + } + } + + @Test + public void testMarkReset() throws Exception { + assertEquals('a', tee.read()); + tee.mark(1); + assertEquals('b', tee.read()); + tee.reset(); + assertEquals('b', tee.read()); + assertEquals('c', tee.read()); + assertEquals(-1, tee.read()); + assertEquals("abbc", output.toString()); + } + + @Test + public void testReadEverything() throws Exception { + assertEquals('a', tee.read()); + assertEquals('b', tee.read()); + assertEquals('c', tee.read()); + assertEquals(-1, tee.read()); + assertEquals("abc", output.toString()); + } + + @Test + public void testReadNothing() throws Exception { + assertEquals("", output.toString()); + } + + @Test + public void testReadOneChar() throws Exception { + assertEquals('a', tee.read()); + assertEquals("a", output.toString()); + } + + @Test + public void testReadToArray() throws Exception { + final char[] buffer = new char[8]; + assertEquals(3, tee.read(buffer)); + assertEquals('a', buffer[0]); + assertEquals('b', buffer[1]); + assertEquals('c', buffer[2]); + assertEquals(-1, tee.read(buffer)); + assertEquals("abc", output.toString()); + } + + @Test + public void testReadToArrayWithOffset() throws Exception { + final char[] buffer = new char[8]; + assertEquals(3, tee.read(buffer, 4, 4)); + assertEquals('a', buffer[4]); + assertEquals('b', buffer[5]); + assertEquals('c', buffer[6]); + assertEquals(-1, tee.read(buffer, 4, 4)); + assertEquals("abc", output.toString()); + } + + @Test + public void testReadToCharBuffer() throws Exception { + final CharBuffer buffer = CharBuffer.allocate(8); + buffer.position(1); + assertEquals(3, tee.read(buffer)); + assertEquals(4, buffer.position()); + buffer.flip(); + buffer.position(1); + assertEquals('a', buffer.charAt(0)); + assertEquals('b', buffer.charAt(1)); + assertEquals('c', buffer.charAt(2)); + assertEquals(-1, tee.read(buffer)); + assertEquals("abc", output.toString()); + } + + @Test + public void testSkip() throws Exception { + assertEquals('a', tee.read()); + assertEquals(1, tee.skip(1)); + assertEquals('c', tee.read()); + assertEquals(-1, tee.read()); + assertEquals("ac", output.toString()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java b/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java index 0e6073a..6f63879 100644 --- a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java +++ b/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java @@ -19,16 +19,23 @@ package org.apache.commons.io.testtools; import java.io.IOException; import java.io.InputStream; +import org.apache.commons.io.input.NullInputStream; import org.apache.commons.io.input.ProxyInputStream; -import junit.framework.AssertionFailedError; - /** - * Helper class for checking behaviour of IO classes. + * Helper class for checking behavior of IO classes. */ public class YellOnCloseInputStream extends ProxyInputStream { /** + * Default ctor. + */ + @SuppressWarnings("resource") + public YellOnCloseInputStream() { + super(new NullInputStream()); + } + + /** * @param proxy InputStream to delegate to. */ public YellOnCloseInputStream(final InputStream proxy) { @@ -38,7 +45,7 @@ public class YellOnCloseInputStream extends ProxyInputStream { /** @see java.io.InputStream#close() */ @Override public void close() throws IOException { - throw new AssertionFailedError("close() was called on OutputStream"); + throw new IOException("close() was called on OutputStream"); } } diff --git a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java b/src/test/java/org/apache/commons/io/testtools/YellOnCloseOutputStream.java similarity index 61% copy from src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java copy to src/test/java/org/apache/commons/io/testtools/YellOnCloseOutputStream.java index 0e6073a..4737cb9 100644 --- a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java +++ b/src/test/java/org/apache/commons/io/testtools/YellOnCloseOutputStream.java @@ -17,28 +17,34 @@ package org.apache.commons.io.testtools; import java.io.IOException; -import java.io.InputStream; +import java.io.OutputStream; -import org.apache.commons.io.input.ProxyInputStream; - -import junit.framework.AssertionFailedError; +import org.apache.commons.io.output.NullOutputStream; +import org.apache.commons.io.output.ProxyOutputStream; /** - * Helper class for checking behaviour of IO classes. + * Helper class for checking behavior of IO classes. */ -public class YellOnCloseInputStream extends ProxyInputStream { +public class YellOnCloseOutputStream extends ProxyOutputStream { + + /** + * Default ctor. + */ + public YellOnCloseOutputStream() { + super(NullOutputStream.NULL_OUTPUT_STREAM); + } /** - * @param proxy InputStream to delegate to. + * @param proxy OutputStream to delegate to. */ - public YellOnCloseInputStream(final InputStream proxy) { + public YellOnCloseOutputStream(final OutputStream proxy) { super(proxy); } - /** @see java.io.InputStream#close() */ + /** @see java.io.OutputStream#close() */ @Override public void close() throws IOException { - throw new AssertionFailedError("close() was called on OutputStream"); + throw new IOException("close() was called on OutputStream"); } } diff --git a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java b/src/test/java/org/apache/commons/io/testtools/YellOnCloseReader.java similarity index 63% copy from src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java copy to src/test/java/org/apache/commons/io/testtools/YellOnCloseReader.java index 0e6073a..8ce7075 100644 --- a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java +++ b/src/test/java/org/apache/commons/io/testtools/YellOnCloseReader.java @@ -17,28 +17,35 @@ package org.apache.commons.io.testtools; import java.io.IOException; -import java.io.InputStream; +import java.io.Reader; -import org.apache.commons.io.input.ProxyInputStream; - -import junit.framework.AssertionFailedError; +import org.apache.commons.io.input.NullReader; +import org.apache.commons.io.input.ProxyReader; /** - * Helper class for checking behaviour of IO classes. + * Helper class for checking behavior of IO classes. */ -public class YellOnCloseInputStream extends ProxyInputStream { +public class YellOnCloseReader extends ProxyReader { + + /** + * Default ctor. + */ + @SuppressWarnings("resource") + public YellOnCloseReader() { + super(new NullReader()); + } /** - * @param proxy InputStream to delegate to. + * @param proxy Reader to delegate to. */ - public YellOnCloseInputStream(final InputStream proxy) { + public YellOnCloseReader(final Reader proxy) { super(proxy); } - /** @see java.io.InputStream#close() */ + /** @see java.io.Reader#close() */ @Override public void close() throws IOException { - throw new AssertionFailedError("close() was called on OutputStream"); + throw new IOException("close() was called on OutputStream"); } } diff --git a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java b/src/test/java/org/apache/commons/io/testtools/YellOnCloseWriter.java similarity index 68% copy from src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java copy to src/test/java/org/apache/commons/io/testtools/YellOnCloseWriter.java index 0e6073a..026bef2 100644 --- a/src/test/java/org/apache/commons/io/testtools/YellOnCloseInputStream.java +++ b/src/test/java/org/apache/commons/io/testtools/YellOnCloseWriter.java @@ -17,28 +17,34 @@ package org.apache.commons.io.testtools; import java.io.IOException; -import java.io.InputStream; +import java.io.Writer; -import org.apache.commons.io.input.ProxyInputStream; - -import junit.framework.AssertionFailedError; +import org.apache.commons.io.output.NullWriter; +import org.apache.commons.io.output.ProxyWriter; /** * Helper class for checking behaviour of IO classes. */ -public class YellOnCloseInputStream extends ProxyInputStream { +public class YellOnCloseWriter extends ProxyWriter { + + /** + * Default ctor. + */ + public YellOnCloseWriter() { + super(NullWriter.NULL_WRITER); + } /** - * @param proxy InputStream to delegate to. + * @param proxy Writer to delegate to. */ - public YellOnCloseInputStream(final InputStream proxy) { + public YellOnCloseWriter(final Writer proxy) { super(proxy); } - /** @see java.io.InputStream#close() */ + /** @see java.io.Writer#close() */ @Override public void close() throws IOException { - throw new AssertionFailedError("close() was called on OutputStream"); + throw new IOException("close() was called on OutputStream"); } }