IO-94 - New MockReader implementation

git-svn-id: https://svn.apache.org/repos/asf/jakarta/commons/proper/io/trunk@454248 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/java/org/apache/commons/io/input/MockReader.java b/src/java/org/apache/commons/io/input/MockReader.java
new file mode 100644
index 0000000..1085c3a
--- /dev/null
+++ b/src/java/org/apache/commons/io/input/MockReader.java
@@ -0,0 +1,293 @@
+/*
+ * 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.EOFException;
+import java.io.IOException;
+import java.io.Reader;
+
+/**
+ * A mock {@link Reader} for testing purposes.
+ * <p>
+ * This implementation provides a light weight mock
+ * object for testing with a {@link Reader}
+ * where the contents don't matter.
+ * <p>
+ * One use case would be for testing the handling of
+ * large {@link Reader} as it can emulate that
+ * scenario without the overhead of actually processing
+ * large numbers of characters - significantly speeding up
+ * test execution times.
+ * <p>
+ * Alternatively, if some kind of data is required as part
+ * of a test the <code>processChar()</code> and
+ * <code>processChars()</code> methods can be implemented to generate
+ * test data.
+ *
+ *
+ * @since Commons IO 1.3
+ * @version $Revision$
+ */
+public class MockReader extends Reader {
+
+    private long size;
+    private long position;
+    private long mark = -1;
+    private long readlimit;
+    private boolean eof;
+    private boolean throwEofException;
+    private boolean markSupported;
+
+    /**
+     * Create a mock {@link Reader} of the specified size.
+     *
+     * @param size The size of the mock Reader.
+     */
+    public MockReader(long size) {
+       this(size, true, false);
+    }
+
+    /**
+     * Create a mock {@link Reader} of the specified
+     * size and option settings.
+     *
+     * @param size The size of the mock Reader.
+     * @param markSupported Whether this instance will support
+     * the <code>mark()</code> functionality.
+     * @param throwEofException Whether this implementation
+     * will throw an {@link EOFException} or return -1 when the
+     * end of file is reached.
+     */
+    public MockReader(long size, boolean markSupported, boolean throwEofException) {
+       this.size = size;
+       this.markSupported = markSupported;
+       this.throwEofException = throwEofException;
+    }
+
+    /**
+     * Return the current position.
+     *
+     * @return the current position.
+     */
+    public long getPosition() {
+        return position;
+    }
+
+    /**
+     * Return the size of this Mock {@link Reader}
+     *
+     * @return the size of the mock Reader.
+     */
+    public long getSize() {
+        return size;
+    }
+
+    /**
+     * Close this Reader - resets the internal state to
+     * the initial values.
+     *
+     * @throws IOException If an error occurs.
+     */
+    public void close() throws IOException {
+        eof = false;
+        position = 0;
+        mark = -1;
+    }
+
+    /**
+     * Mark the current position.
+     *
+     * @param readlimit The number of characters before this marked position
+     * is invalid.
+     * @throws UnsupportedOperationException if mark is not supported.
+     */
+    public synchronized void mark(int readlimit) {
+        if (!markSupported) {
+            throw new UnsupportedOperationException("Mark not supported");
+        }
+        mark = position;
+        this.readlimit = readlimit;
+    }
+
+    /**
+     * Indicates whether <i>mark</i> is supported.
+     *
+     * @return Whether <i>mark</i> is supported or not.
+     */
+    public boolean markSupported() {
+        return markSupported;
+    }
+
+    /**
+     * Read a character.
+     *
+     * @return Either The character value returned by <code>processChar()</code>
+     * or <code>-1</code> if the end of file has been reached and
+     * <code>throwEofException</code> is set to <code>false</code>.
+     * @throws EOFException if the end of file is reached and
+     * <code>throwEofException</code> is set to <code>true</code>.
+     * @throws IOException if trying to read past the end of file.
+     */
+    public int read() throws IOException {
+        if (eof) {
+            throw new IOException("Read after end of file");
+        }
+        if (position == size) {
+            return doEndOfFile();
+        }
+        position++;
+        return processChar();
+    }
+
+    /**
+     * Read some characters into the specified array.
+     *
+     * @param chars The character array to read into
+     * @return The number of characters read or <code>-1</code>
+     * if the end of file has been reached and
+     * <code>throwEofException</code> is set to <code>false</code>.
+     * @throws EOFException if the end of file is reached and
+     * <code>throwEofException</code> is set to <code>true</code>.
+     * @throws IOException if trying to read past the end of file.
+     */
+    public int read(char[] chars) throws IOException {
+        return read(chars, 0, chars.length);
+    }
+
+    /**
+     * Read the specified number characters into an array.
+     *
+     * @param chars The character array to read into.
+     * @param offset The offset to start reading characters into.
+     * @param length The number of characters to read.
+     * @return The number of characters read or <code>-1</code>
+     * if the end of file has been reached and
+     * <code>throwEofException</code> is set to <code>false</code>.
+     * @throws EOFException if the end of file is reached and
+     * <code>throwEofException</code> is set to <code>true</code>.
+     * @throws IOException if trying to read past the end of file.
+     */
+    public int read(char[] chars, int offset, int length) throws IOException {
+        if (eof) {
+            throw new IOException("Read after end of file");
+        }
+        if (position == size) {
+            return doEndOfFile();
+        }
+        position += length;
+        int returnLength = length;
+        if (position > size) {
+            returnLength = length - (int)(position - size);
+            position = size;
+        }
+        processChars(chars, offset, returnLength);
+        return returnLength;
+    }
+
+    /**
+     * Reset the stream to the point when mark was last called.
+     *
+     * @throws UnsupportedOperationException if mark is not supported.
+     * @throws IOException If no position has been marked
+     * or the read limit has been exceed since the last position was
+     * marked.
+     */
+    public synchronized void reset() throws IOException {
+        if (!markSupported) {
+            throw new UnsupportedOperationException("Mark not supported");
+        }
+        if (mark < 0) {
+            throw new IOException("No position has been marked");
+        }
+        if (position > (mark + readlimit)) {
+            throw new IOException("Marked position [" + mark +
+                    "] is no longer valid - passed the read limit [" +
+                    readlimit + "]");
+        }
+        position = mark;
+        eof = false;
+    }
+
+    /**
+     * Skip a specified number of characters.
+     *
+     * @param numberOfChars The number of characters to skip.
+     * @return The number of characters skipped or <code>-1</code>
+     * if the end of file has been reached and
+     * <code>throwEofException</code> is set to <code>false</code>.
+     * @throws EOFException if the end of file is reached and
+     * <code>throwEofException</code> is set to <code>true</code>.
+     * @throws IOException if trying to read past the end of file.
+     */
+    public long skip(long numberOfChars) throws IOException {
+        if (eof) {
+            throw new IOException("Skip after end of file");
+        }
+        if (position == size) {
+            return doEndOfFile();
+        }
+        position += numberOfChars;
+        long returnLength = numberOfChars;
+        if (position > size) {
+            returnLength = numberOfChars - (position - size);
+            position = size;
+        }
+        return returnLength;
+    }
+
+    /**
+     * Return a character value for the  <code>read()</code> method.
+     * <p>
+     * <strong>N.B.</strong> This implementation returns
+     * zero.
+     *
+     * @return This implementation always returns zero.
+     */
+    protected int processChar() {
+        return 0;
+    }
+
+    /**
+     * Process the characters for the <code>read(char[], offset, length)</code>
+     * method.
+     * <p>
+     * <strong>N.B.</strong> This implementation leaves the character
+     * array unchanged.
+     *
+     * @param chars The character array
+     * @param offset The offset to start at.
+     * @param length The number of characters.
+     */
+    protected void processChars(char[] chars, int offset, int length) {
+    }
+
+    /**
+     * Handle End of File.
+     *
+     * @return <code>-1</code> if <code>throwEofException</code> is
+     * set to <code>false</code>
+     * @throws EOFException if <code>throwEofException</code> is set
+     * to <code>true</code>.
+     */
+    private int doEndOfFile() throws EOFException {
+        eof = true;
+        if (throwEofException) {
+            throw new EOFException();
+        }
+        return -1;
+    }
+}
diff --git a/src/test/org/apache/commons/io/input/MockReaderTestCase.java b/src/test/org/apache/commons/io/input/MockReaderTestCase.java
new file mode 100644
index 0000000..7b2ecbd
--- /dev/null
+++ b/src/test/org/apache/commons/io/input/MockReaderTestCase.java
@@ -0,0 +1,253 @@
+/*
+ * 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.EOFException;
+import java.io.IOException;
+import java.io.Reader;
+
+import junit.framework.TestCase;
+
+/**
+ * JUnit Test Case for {@link MockReader}.
+ *
+ * @version $Id$
+ */
+public class MockReaderTestCase extends TestCase {
+
+    /** Constructor */
+    public MockReaderTestCase(String name) {
+        super(name);
+    }
+
+    /** Set up */
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    /** Tear Down */
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Test <code>available()</code> method.
+     */
+    public void testRead() throws Exception {
+        int size = 5;
+        TestMockReader reader = new TestMockReader(size);
+        for (int i = 0; i < size; i++) {
+            assertEquals("Check Value [" + i + "]", i, reader.read());
+        }
+
+        // Check End of File
+        assertEquals("End of File", -1, reader.read());
+
+        // Test reading after the end of file
+        try {
+            int result = reader.read();
+            fail("Should have thrown an IOException, value=[" + result + "]");
+        } catch (IOException e) {
+            assertEquals("Read after end of file", e.getMessage());
+        }
+
+        // Close - should reset
+        reader.close();
+        assertEquals("Available after close", 0, reader.getPosition());
+    }
+
+    /**
+     * Test <code>read(char[])</code> method.
+     */
+    public void testReadCharArray() throws Exception {
+        char[] chars = new char[10];
+        Reader reader = new TestMockReader(15);
+
+        // Read into array
+        int count1 = reader.read(chars);
+        assertEquals("Read 1", chars.length, count1);
+        for (int i = 0; i < count1; i++) {
+            assertEquals("Check Chars 1", i, chars[i]);
+        }
+
+        // Read into array
+        int count2 = reader.read(chars);
+        assertEquals("Read 2", 5, count2);
+        for (int i = 0; i < count2; i++) {
+            assertEquals("Check Chars 2", count1 + i, chars[i]);
+        }
+
+        // End of File
+        int count3 = reader.read(chars);
+        assertEquals("Read 3 (EOF)", -1, count3);
+
+        // Test reading after the end of file
+        try {
+            int count4 = reader.read(chars);
+            fail("Should have thrown an IOException, value=[" + count4 + "]");
+        } catch (IOException e) {
+            assertEquals("Read after end of file", e.getMessage());
+        }
+
+        // reset by closing
+        reader.close();
+    
+        // Read into array using offset & length
+        int offset = 2;
+        int lth    = 4;
+        int count5 = reader.read(chars, offset, lth);
+        assertEquals("Read 5", lth, count5);
+        for (int i = offset; i < lth; i++) {
+            assertEquals("Check Chars 3", i, chars[i]);
+        }
+    }
+
+    /**
+     * Test when configured to throw an EOFException at the end of file
+     * (rather than return -1).
+     */
+    public void testEOFException() throws Exception {
+        Reader reader = new TestMockReader(2, false, true);
+        assertEquals("Read 1",  0, reader.read());
+        assertEquals("Read 2",  1, reader.read());
+        try {
+            int result = reader.read();
+            fail("Should have thrown an EOFException, value=[" + result + "]");
+        } catch (EOFException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Test <code>mark()</code> and <code>reset()</code> methods.
+     */
+    public void testMarkAndReset() throws Exception {
+        int position = 0;
+        int readlimit = 10;
+        Reader reader = new TestMockReader(100, true, false);
+        
+        assertTrue("Mark Should be Supported", reader.markSupported());
+
+        // No Mark
+        try {
+            reader.reset();
+            fail("Read limit exceeded, expected IOException ");
+        } catch (Exception e) {
+            assertEquals("No Mark IOException message",
+                         "No position has been marked",
+                         e.getMessage());
+        }
+
+        for (; position < 3; position++) {
+            assertEquals("Read Before Mark [" + position +"]",  position, reader.read());
+        }
+
+        // Mark
+        reader.mark(readlimit);
+
+        // Read further
+        for (int i = 0; i < 3; i++) {
+            assertEquals("Read After Mark [" + i +"]",  (position + i), reader.read());
+        }
+
+        // Reset
+        reader.reset();
+
+        // Read From marked position
+        for (int i = 0; i < readlimit + 1; i++) {
+            assertEquals("Read After Reset [" + i +"]",  (position + i), reader.read());
+        }
+
+        // Reset after read limit passed
+        try {
+            reader.reset();
+            fail("Read limit exceeded, expected IOException ");
+        } catch (Exception e) {
+            assertEquals("Read limit IOException message",
+                         "Marked position [" + position
+                         + "] is no longer valid - passed the read limit ["
+                         + readlimit + "]",
+                         e.getMessage());
+        }
+    }
+
+    /**
+     * Test <code>mark()</code> not supported.
+     */
+    public void testMarkNotSupported() throws Exception {
+        Reader reader = new TestMockReader(100, false, true);
+        assertFalse("Mark Should NOT be Supported", reader.markSupported());
+
+        try {
+            reader.mark(5);
+            fail("mark() should throw UnsupportedOperationException");
+        } catch (UnsupportedOperationException e) {
+            assertEquals("mark() error message",  "Mark not supported", e.getMessage());
+        }
+
+        try {
+            reader.reset();
+            fail("reset() should throw UnsupportedOperationException");
+        } catch (UnsupportedOperationException e) {
+            assertEquals("reset() error message",  "Mark not supported", e.getMessage());
+        }
+    }
+
+    /**
+     * Test <code>skip()</code> method.
+     */
+   public void testSkip() throws Exception {
+        int skip = 20;
+        Reader reader = new TestMockReader(10, true, false);
+        assertEquals("Read 1", 0, reader.read());
+        assertEquals("Read 2", 1, reader.read());
+        assertEquals("Skip 1", 5, reader.skip(5));
+        assertEquals("Read 3", 7, reader.read());
+        assertEquals("Skip 2", 2, reader.skip(5)); // only 2 left to skip
+        assertEquals("Skip 3 (EOF)", -1, reader.skip(5)); // End of file
+        try {
+            reader.skip(5); //
+            fail("Expected IOException for skipping after end of file");
+        } catch (Exception e) {
+            assertEquals("Skip after EOF IOException message",
+                    "Skip after end of file",
+                    e.getMessage());
+        }
+    }
+
+
+    // ------------- Test MockReader implementation -------------
+
+    private static final class TestMockReader extends MockReader {
+        public TestMockReader(int size) {
+            super(size);
+        }
+        public TestMockReader(int size, boolean markSupported, boolean throwEofException) {
+            super(size, markSupported, throwEofException);
+        }
+        protected int processChar() {
+            return ((int)getPosition() - 1);
+        }
+        protected void processChars(char[] chars, int offset, int length) {
+            int startPos = (int)getPosition() - length;
+            for (int i = offset; i < length; i++) {
+                chars[i] = (char)(startPos + i);
+            }
+        }
+        
+    }
+}