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 c44fb5163 Add UnsynchronizedBufferedReader and UnsynchronizedReader c44fb5163 is described below commit c44fb516390012f9c9e9996ba9162331f8fe7ecb Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Sat Sep 14 10:11:43 2024 -0400 Add UnsynchronizedBufferedReader and UnsynchronizedReader --- src/changes/changes.xml | 2 + .../io/input/UnsynchronizedBufferedReader.java | 463 +++++++++++++++++++++ .../commons/io/input/UnsynchronizedReader.java | 78 ++++ .../io/input/UnsynchronizedBufferedReaderTest.java | 457 ++++++++++++++++++++ 4 files changed, 1000 insertions(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 49a1d78c3..e4338d4c1 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -54,6 +54,8 @@ The <action> type attribute can be add,update,fix,remove. <action dev="ggregory" type="add" due-to="Gary Gregory">Add CloseShieldInputStream.systemIn(InputStream).</action> <action dev="ggregory" type="add" due-to="Gary Gregory">Add NullInputStream.init().</action> <action dev="ggregory" type="add" due-to="Gary Gregory">Add AbstractInputStream and refactor duplicate code.</action> + <action dev="ggregory" type="add" due-to="Gary Gregory">Add UnsynchronizedReader.</action> + <action dev="ggregory" type="add" due-to="Gary Gregory">Add UnsynchronizedBufferedReader.</action> <!-- FIX --> <action dev="sebb" type="fix" issue="IO-858">FileUtilsWaitForTest does not test anything useful.</action> <action dev="ggregory" type="fix" due-to="Gary Gregory">Add missing unit tests.</action> diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedReader.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedReader.java new file mode 100644 index 000000000..bab791ac2 --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedReader.java @@ -0,0 +1,463 @@ +/* + * 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 static org.apache.commons.io.IOUtils.CR; +import static org.apache.commons.io.IOUtils.LF; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Reader; + +import org.apache.commons.io.IOUtils; + +/** + * Wraps an existing {@link Reader} and buffers the input <em>without any synchronization</em>. Expensive interaction with the underlying reader is minimized, + * since most (smaller) requests can be satisfied by accessing the buffer alone. The drawback is that some extra space is required to hold the buffer and that + * copying takes place when filling that buffer, but this is usually outweighed by the performance benefits. + * <p> + * A typical application pattern for the class looks like this: + * </p> + * + * <pre>{@code + * UnsynchronizedBufferedReader buf = new UnsynchronizedBufferedReader(new FileReader("file")); + * }</pre> + * <p> + * Provenance: Apache Harmony's java.io.BufferedReader, renamed, and modified. + * </p> + * + * @see BufferedReader + * @see BufferedWriter + * @since 2.17.0 + */ +public class UnsynchronizedBufferedReader extends UnsynchronizedReader { + + private static final char NUL = '\0'; + + private final Reader in; + + /** + * The characters that can be read and refilled in bulk. We maintain three indices into this buffer: + * + * <pre> + * { X X X X X X X X X X X X - - } + * ^ ^ ^ + * | | | + * mark pos end + * </pre> + * <p> + * Pos points to the next readable character. End is one greater than the last readable character. When {@code pos == end}, the buffer is empty and must be + * {@link #fillBuf() filled} before characters can be read. + * </p> + * <p> + * Mark is the value pos will be set to on calls to {@link #reset()}. Its value is in the range {@code [0...pos]}. If the mark is {@code -1}, the buffer + * cannot be reset. + * </p> + * <p> + * MarkLimit limits the distance between the mark and the pos. When this limit is exceeded, {@link #reset()} is permitted (but not required) to throw an + * exception. For shorter distances, {@link #reset()} shall not throw (unless the reader is closed). + * </p> + */ + private char[] buf; + + private int pos; + + private int end; + + private int mark = -1; + + private int markLimit = -1; + + /** + * Constructs a new BufferedReader on the Reader {@code in}. The buffer gets the default size (8 KB). + * + * @param in the Reader that is buffered. + */ + public UnsynchronizedBufferedReader(final Reader in) { + this.in = in; + buf = new char[IOUtils.DEFAULT_BUFFER_SIZE]; + } + + /** + * Constructs a new BufferedReader on the Reader {@code in}. The buffer size is specified by the parameter {@code size}. + * + * @param in the Reader that is buffered. + * @param size the size of the buffer to allocate. + * @throws IllegalArgumentException if {@code size <= 0}. + */ + public UnsynchronizedBufferedReader(final Reader in, final int size) { + if (size <= 0) { + throw new IllegalArgumentException("size <= 0"); + } + this.in = in; + buf = new char[size]; + } + + private void checkClosed() throws IOException { + if (isClosed()) { + throw new IOException("Closed"); + } + } + + /** + * Peeks at the next input character, refilling the buffer if necessary. If this character is a newline character ("\n"), it is discarded. + */ + final void chompNewline() throws IOException { + if ((pos != end || fillBuf() != EOF) && buf[pos] == LF) { + pos++; + } + } + + /** + * Closes this reader. This implementation closes the buffered source reader and releases the buffer. Nothing is done if this reader has already been + * closed. + * + * @throws IOException if an error occurs while closing this reader. + */ + @Override + public void close() throws IOException { + if (!isClosed()) { + in.close(); + buf = null; + } + } + + /** + * Populates the buffer with data. It is an error to call this method when the buffer still contains data; ie. if {@code pos < end}. + * + * @return the number of bytes read into the buffer, or -1 if the end of the source stream has been reached. + */ + private int fillBuf() throws IOException { + // assert(pos == end); + + if (mark == EOF || pos - mark >= markLimit) { + /* mark isn't set or has exceeded its limit. use the whole buffer */ + final int result = in.read(buf, 0, buf.length); + if (result > 0) { + mark = -1; + pos = 0; + end = result; + } + return result; + } + + if (mark == 0 && markLimit > buf.length) { + /* the only way to make room when mark=0 is by growing the buffer */ + int newLength = buf.length * 2; + if (newLength > markLimit) { + newLength = markLimit; + } + final char[] newbuf = new char[newLength]; + System.arraycopy(buf, 0, newbuf, 0, buf.length); + buf = newbuf; + } else if (mark > 0) { + /* make room by shifting the buffered data to left mark positions */ + System.arraycopy(buf, mark, buf, 0, buf.length - mark); + pos -= mark; + end -= mark; + mark = 0; + } + + /* Set the new position and mark position */ + final int count = in.read(buf, pos, buf.length - pos); + if (count != EOF) { + end += count; + } + return count; + } + + /** + * Tests whether or not this reader is closed. + * + * @return {@code true} if this reader is closed, {@code false} otherwise. + */ + private boolean isClosed() { + return buf == null; + } + + /** + * Sets a mark position in this reader. The parameter {@code markLimit} indicates how many characters can be read before the mark is invalidated. Calling + * {@link #reset()} will reposition the reader back to the marked position if {@code markLimit} has not been surpassed. + * + * @param markLimit the number of characters that can be read before the mark is invalidated. + * @throws IllegalArgumentException if {@code markLimit < 0}. + * @throws IOException if an error occurs while setting a mark in this reader. + * @see #markSupported() + * @see #reset() + */ + @Override + public void mark(final int markLimit) throws IOException { + if (markLimit < 0) { + throw new IllegalArgumentException(); + } + checkClosed(); + this.markLimit = markLimit; + mark = pos; + } + + /** + * Tests whether this reader supports the {@link #mark(int)} and {@link #reset()} methods. This implementation returns {@code true}. + * + * @return {@code true} for {@code BufferedReader}. + * @see #mark(int) + * @see #reset() + */ + @Override + public boolean markSupported() { + return true; + } + + /** + * Reads a single character from this reader and returns it with the two higher-order bytes set to 0. If possible, BufferedReader returns a character from + * the buffer. If there are no characters available in the buffer, it fills the buffer and then returns a character. It returns -1 if there are no more + * characters in the source reader. + * + * @return the character read or -1 if the end of the source reader has been reached. + * @throws IOException if this reader is closed or some other I/O error occurs. + */ + @Override + public int read() throws IOException { + checkClosed(); + /* Are there buffered characters available? */ + if (pos < end || fillBuf() != EOF) { + return buf[pos++]; + } + return EOF; + } + + /** + * Reads at most {@code length} characters from this reader and stores them at {@code offset} in the character array {@code buffer}. Returns the number of + * characters actually read or -1 if the end of the source reader has been reached. If all the buffered characters have been used, a mark has not been set + * and the requested number of characters is larger than this readers buffer size, BufferedReader bypasses the buffer and simply places the results directly + * into {@code buffer}. + * + * @param buffer the character array to store the characters read. + * @param offset the initial position in {@code buffer} to store the bytes read from this reader. + * @param length the maximum number of characters to read, must be non-negative. + * @return number of characters read or -1 if the end of the source reader has been reached. + * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code length < 0}, or if {@code offset + length} is greater than the size of {@code buffer}. + * @throws IOException if this reader is closed or some other I/O error occurs. + */ + @Override + public int read(final char[] buffer, int offset, final int length) throws IOException { + checkClosed(); + if (offset < 0 || offset > buffer.length - length || length < 0) { + throw new IndexOutOfBoundsException(); + } + int outstanding = length; + while (outstanding > 0) { + + /* + * If there are bytes in the buffer, grab those first. + */ + final int available = end - pos; + if (available > 0) { + final int count = available >= outstanding ? outstanding : available; + System.arraycopy(buf, pos, buffer, offset, count); + pos += count; + offset += count; + outstanding -= count; + } + + /* + * Before attempting to read from the underlying stream, make sure we really, really want to. We won't bother if we're done, or if we've already got + * some bytes and reading from the underlying stream would block. + */ + if (outstanding == 0 || outstanding < length && !in.ready()) { + break; + } + + // assert(pos == end); + + /* + * If we're unmarked and the requested size is greater than our buffer, read the bytes directly into the caller's buffer. We don't read into smaller + * buffers because that could result in a many reads. + */ + if ((mark == -1 || pos - mark >= markLimit) && outstanding >= buf.length) { + final int count = in.read(buffer, offset, outstanding); + if (count > 0) { + outstanding -= count; + mark = -1; + } + + break; // assume the source stream gave us all that it could + } + + if (fillBuf() == EOF) { + break; // source is exhausted + } + } + + final int count = length - outstanding; + return count > 0 || count == length ? count : EOF; + } + + /** + * Returns the next line of text available from this reader. A line is represented by zero or more characters followed by {@code LF}, {@code CR}, + * {@code "\r\n"} or the end of the reader. The string does not include the newline sequence. + * + * @return the contents of the line or {@code null} if no characters were read before the end of the reader has been reached. + * @throws IOException if this reader is closed or some other I/O error occurs. + */ + public String readLine() throws IOException { + checkClosed(); + /* has the underlying stream been exhausted? */ + if (pos == end && fillBuf() == EOF) { + return null; + } + for (int charPos = pos; charPos < end; charPos++) { + final char ch = buf[charPos]; + if (ch > CR) { + continue; + } + if (ch == LF) { + final String res = new String(buf, pos, charPos - pos); + pos = charPos + 1; + return res; + } else if (ch == CR) { + final String res = new String(buf, pos, charPos - pos); + pos = charPos + 1; + if ((pos < end || fillBuf() != EOF) && buf[pos] == LF) { + pos++; + } + return res; + } + } + + char eol = NUL; + final StringBuilder result = new StringBuilder(80); + /* Typical Line Length */ + + result.append(buf, pos, end - pos); + while (true) { + pos = end; + + /* Are there buffered characters available? */ + if (eol == LF) { + return result.toString(); + } + // attempt to fill buffer + if (fillBuf() == EOF) { + // characters or null. + return result.length() > 0 || eol != NUL ? result.toString() : null; + } + for (int charPos = pos; charPos < end; charPos++) { + final char c = buf[charPos]; + if (eol == NUL) { + if (c == LF || c == CR) { + eol = c; + } + } else { + if (eol == CR && c == LF) { + if (charPos > pos) { + result.append(buf, pos, charPos - pos - 1); + } + pos = charPos + 1; + } else { + if (charPos > pos) { + result.append(buf, pos, charPos - pos - 1); + } + pos = charPos; + } + return result.toString(); + } + } + if (eol == NUL) { + result.append(buf, pos, end - pos); + } else { + result.append(buf, pos, end - pos - 1); + } + } + } + + /** + * Tests whether this reader is ready to be read without blocking. + * + * @return {@code true} if this reader will not block when {@code read} is called, {@code false} if unknown or blocking will occur. + * @throws IOException if this reader is closed or some other I/O error occurs. + * @see #read() + * @see #read(char[], int, int) + * @see #readLine() + */ + @Override + public boolean ready() throws IOException { + checkClosed(); + return end - pos > 0 || in.ready(); + } + + /** + * Resets this reader's position to the last {@code mark()} location. Invocations of {@code read()} and {@code skip()} will occur from this new location. + * + * @throws IOException if this reader is closed or no mark has been set. + * @see #mark(int) + * @see #markSupported() + */ + @Override + public void reset() throws IOException { + checkClosed(); + if (mark == -1) { + throw new IOException("mark == -1"); + } + pos = mark; + } + + /** + * Skips {@code amount} characters in this reader. Subsequent {@code read()}s will not return these characters unless {@code reset()} is used. Skipping + * characters may invalidate a mark if {@code markLimit} is surpassed. + * + * @param amount the maximum number of characters to skip. + * @return the number of characters actually skipped. + * @throws IllegalArgumentException if {@code amount < 0}. + * @throws IOException if this reader is closed or some other I/O error occurs. + * @see #mark(int) + * @see #markSupported() + * @see #reset() + */ + @Override + public long skip(final long amount) throws IOException { + if (amount < 0) { + throw new IllegalArgumentException(); + } + checkClosed(); + if (amount < 1) { + return 0; + } + if (end - pos >= amount) { + pos += amount; + return amount; + } + + long read = end - pos; + pos = end; + while (read < amount) { + if (fillBuf() == EOF) { + return read; + } + if (end - pos >= amount - read) { + pos += amount - read; + return amount; + } + // Couldn't get all the characters, skip what we read + read += end - pos; + pos = end; + } + return amount; + } +} diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedReader.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedReader.java new file mode 100644 index 000000000..1350d2153 --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedReader.java @@ -0,0 +1,78 @@ +/* + * 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 org.apache.commons.io.IOUtils; + +/** + * A {@link Reader} without any of the superclass' synchronization. + * + * @since 2.17.0 + */ +public abstract class UnsynchronizedReader extends Reader { + + /** + * The maximum skip-buffer size. + */ + private static final int MAX_SKIP_BUFFER_SIZE = IOUtils.DEFAULT_BUFFER_SIZE; + + /** + * The skip buffer, defaults to null until allocated in {@link UnsynchronizedReader#skip(long)}. + */ + private char skipBuffer[]; + + /** + * Skips characters by reading from this instance. + * + * This method will <em>block</em> until: + * <ul> + * <li>some characters are available,</li> + * <li>an I/O error occurs, or</li> + * <li>the end of the stream is reached.</li> + * </ul> + * + * @param n The number of characters to skip. + * @return The number of characters actually skipped. + * @throws IllegalArgumentException If {@code n} is negative. + * @throws IOException If an I/O error occurs. + */ + @Override + public long skip(final long n) throws IOException { + if (n < 0L) { + throw new IllegalArgumentException("skip value < 0"); + } + final int bufSize = (int) Math.min(n, MAX_SKIP_BUFFER_SIZE); + if (skipBuffer == null || skipBuffer.length < bufSize) { + skipBuffer = new char[bufSize]; + } + long remaining = n; + while (remaining > 0) { + final int countOrEof = read(skipBuffer, 0, (int) Math.min(remaining, bufSize)); + if (countOrEof == EOF) { + break; + } + remaining -= countOrEof; + } + return n - remaining; + } +} diff --git a/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedReaderTest.java b/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedReaderTest.java new file mode 100644 index 000000000..909a20cc0 --- /dev/null +++ b/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedReaderTest.java @@ -0,0 +1,457 @@ +/* + * 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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.CharArrayReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PipedReader; +import java.io.Reader; +import java.io.StringReader; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link UnsynchronizedBufferedReader}. + * <p> + * Provenance: Apache Harmony {@code BufferedReaderTest}, copied, and modified. + * </p> + */ +public class UnsynchronizedBufferedReaderTest { + + private UnsynchronizedBufferedReader br; + + private final String testString = "Test_All_Tests\nTest_java_io_BufferedInputStream\nTest_java_io_BufferedOutputStream\nTest_java_io_ByteArrayInputStream\n" + + "Test_java_io_ByteArrayOutputStream\nTest_java_io_DataInputStream\nTest_java_io_File\nTest_java_io_FileDescriptor\nTest_java_io_FileInputStream\n" + + "Test_java_io_FileNotFoundException\nTest_java_io_FileOutputStream\nTest_java_io_FilterInputStream\nTest_java_io_FilterOutputStream\n" + + "Test_java_io_InputStream\nTest_java_io_IOException\nTest_java_io_OutputStream\nTest_java_io_PrintStream\nTest_java_io_RandomAccessFile\n" + + "Test_java_io_SyncFailedException\nTest_java_lang_AbstractMethodError\nTest_java_lang_ArithmeticException\n" + + "Test_java_lang_ArrayIndexOutOfBoundsException\nTest_java_lang_ArrayStoreException\nTest_java_lang_Boolean\nTest_java_lang_Byte\n" + + "Test_java_lang_Character\nTest_java_lang_Class\nTest_java_lang_ClassCastException\nTest_java_lang_ClassCircularityError\n" + + "Test_java_lang_ClassFormatError\nTest_java_lang_ClassLoader\nTest_java_lang_ClassNotFoundException\nTest_java_lang_CloneNotSupportedException\n" + + "Test_java_lang_Double\nTest_java_lang_Error\nTest_java_lang_Exception\nTest_java_lang_ExceptionInInitializerError\nTest_java_lang_Float\n" + + "Test_java_lang_IllegalAccessError\nTest_java_lang_IllegalAccessException\nTest_java_lang_IllegalArgumentException\n" + + "Test_java_lang_IllegalMonitorStateException\nTest_java_lang_IllegalThreadStateException\nTest_java_lang_IncompatibleClassChangeError\n" + + "Test_java_lang_IndexOutOfBoundsException\nTest_java_lang_InstantiationError\nTest_java_lang_InstantiationException\nTest_java_lang_Integer\n" + + "Test_java_lang_InternalError\nTest_java_lang_InterruptedException\nTest_java_lang_LinkageError\nTest_java_lang_Long\nTest_java_lang_Math\n" + + "Test_java_lang_NegativeArraySizeException\nTest_java_lang_NoClassDefFoundError\nTest_java_lang_NoSuchFieldError\n" + + "Test_java_lang_NoSuchMethodError\nTest_java_lang_NullPointerException\nTest_java_lang_Number\nTest_java_lang_NumberFormatException\n" + + "Test_java_lang_Object\nTest_java_lang_OutOfMemoryError\nTest_java_lang_RuntimeException\nTest_java_lang_SecurityManager\nTest_java_lang_Short\n" + + "Test_java_lang_StackOverflowError\nTest_java_lang_String\nTest_java_lang_StringBuffer\nTest_java_lang_StringIndexOutOfBoundsException\n" + + "Test_java_lang_System\nTest_java_lang_Thread\nTest_java_lang_ThreadDeath\nTest_java_lang_ThreadGroup\nTest_java_lang_Throwable\n" + + "Test_java_lang_UnknownError\nTest_java_lang_UnsatisfiedLinkError\nTest_java_lang_VerifyError\nTest_java_lang_VirtualMachineError\n" + + "Test_java_lang_vm_Image\nTest_java_lang_vm_MemorySegment\nTest_java_lang_vm_ROMStoreException\nTest_java_lang_vm_VM\nTest_java_lang_Void\n" + + "Test_java_net_BindException\nTest_java_net_ConnectException\nTest_java_net_DatagramPacket\nTest_java_net_DatagramSocket\n" + + "Test_java_net_DatagramSocketImpl\nTest_java_net_InetAddress\nTest_java_net_NoRouteToHostException\nTest_java_net_PlainDatagramSocketImpl\n" + + "Test_java_net_PlainSocketImpl\nTest_java_net_Socket\nTest_java_net_SocketException\nTest_java_net_SocketImpl\nTest_java_net_SocketInputStream\n" + + "Test_java_net_SocketOutputStream\nTest_java_net_UnknownHostException\nTest_java_util_ArrayEnumerator\nTest_java_util_Date\n" + + "Test_java_util_EventObject\nTest_java_util_HashEnumerator\nTest_java_util_Hashtable\nTest_java_util_Properties\nTest_java_util_ResourceBundle\n" + + "Test_java_util_tm\nTest_java_util_Vector\n"; + + /** + * Tears down the fixture, for example, close a network connection. This method is called after a test is executed. + */ + @AfterEach + protected void afterEach() { + IOUtils.closeQuietly(br); + } + + private void assertLines(final String in, final String... lines) throws IOException { + try (UnsynchronizedBufferedReader bufferedReader = new UnsynchronizedBufferedReader(new StringReader(in))) { + for (final String line : lines) { + assertEquals(line, bufferedReader.readLine()); + } + assertNull(bufferedReader.readLine()); + } + } + + /** + * Tests {@link UnsynchronizedBufferedReader#close()}. + * + * @throws IOException test failure. + */ + @Test + public void test_close() throws IOException { + // Test for method void UnsynchronizedBufferedReader.close() + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + br.close(); + assertThrows(IOException.class, br::read); + } + + /** + * Tests {@link UnsynchronizedBufferedReader#mark(int)}. + * + * @throws IOException test failure. + */ + @Test + public void test_markI() throws IOException { + // Test for method void UnsynchronizedBufferedReader.mark(int) + char[] buf = null; + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + br.skip(500); + br.mark(1000); + br.skip(250); + br.reset(); + buf = new char[testString.length()]; + br.read(buf, 0, 500); + assertTrue(testString.substring(500, 1000).equals(new String(buf, 0, 500))); + + br = new UnsynchronizedBufferedReader(new StringReader(testString), 800); + br.skip(500); + br.mark(250); + br.read(buf, 0, 1000); + assertThrows(IOException.class, br::reset); + + final char[] chars = new char[256]; + for (int i = 0; i < 256; i++) { + chars[i] = (char) i; + } + + try (Reader in = new UnsynchronizedBufferedReader(new StringReader(new String(chars)), 12)) { + in.skip(6); + in.mark(14); + in.read(new char[14], 0, 14); + in.reset(); + assertTrue(in.read() == (char) 6 && in.read() == (char) 7); + } + try (Reader in = new UnsynchronizedBufferedReader(new StringReader(new String(chars)), 12)) { + in.skip(6); + in.mark(8); + in.skip(7); + in.reset(); + assertTrue(in.read() == (char) 6 && in.read() == (char) 7); + } + try (UnsynchronizedBufferedReader br = new UnsynchronizedBufferedReader(new StringReader("01234"), 2)) { + br.mark(3); + final char[] carray = new char[3]; + final int result = br.read(carray); + assertEquals(3, result); + assertEquals('0', carray[0]); + assertEquals('1', carray[1]); + assertEquals('2', carray[2]); + assertEquals('3', br.read()); + } + try (UnsynchronizedBufferedReader br = new UnsynchronizedBufferedReader(new StringReader("01234"), 2)) { + br.mark(3); + final char[] carray = new char[4]; + final int result = br.read(carray); + assertEquals(4, result); + assertEquals('0', carray[0]); + assertEquals('1', carray[1]); + assertEquals('2', carray[2]); + assertEquals('3', carray[3]); + assertEquals('4', br.read()); + assertEquals(-1, br.read()); + } + try (UnsynchronizedBufferedReader reader = new UnsynchronizedBufferedReader(new StringReader("01234"))) { + reader.mark(Integer.MAX_VALUE); + reader.read(); + } + } + + /** + * Tests {@link UnsynchronizedBufferedReader#markSupported()}. + */ + @Test + public void test_markSupported() { + // Test for method boolean UnsynchronizedBufferedReader.markSupported() + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + assertTrue(br.markSupported()); + } + + /** + * Tests {@link UnsynchronizedBufferedReader#read()}. + * + * @throws IOException test failure. + */ + @Test + public void test_read() throws IOException { + // Test for method int UnsynchronizedBufferedReader.read() + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + final int r = br.read(); + assertTrue(testString.charAt(0) == r); + br = new UnsynchronizedBufferedReader(new StringReader(new String(new char[] { '\u8765' }))); + assertTrue(br.read() == '\u8765'); + // + final char[] chars = new char[256]; + for (int i = 0; i < 256; i++) { + chars[i] = (char) i; + } + try (Reader in = new UnsynchronizedBufferedReader(new StringReader(new String(chars)), 12)) { + assertEquals(0, in.read()); // Fill the + // buffer + final char[] buf = new char[14]; + in.read(buf, 0, 14); // Read greater than the buffer + assertTrue(new String(buf).equals(new String(chars, 1, 14))); + assertEquals(15, in.read()); // Check next byte + } + // + // regression test for HARMONY-841 + try (Reader reader = new UnsynchronizedBufferedReader(new CharArrayReader(new char[5], 1, 0), 2)) { + assertTrue(reader.read() == -1); + } + } + + /** + * Tests {@link UnsynchronizedBufferedReader#read(char[], int, int)}. + * + * @throws IOException test failure. + */ + @Test + public void test_read_$CII_Exception() throws IOException { + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + final char[] nullCharArray = null; + final char[] charArray = testString.toCharArray(); + assertThrows(IndexOutOfBoundsException.class, () -> br.read(nullCharArray, -1, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> br.read(nullCharArray, -1, 0)); + assertThrows(NullPointerException.class, () -> br.read(nullCharArray, 0, -1)); + assertThrows(NullPointerException.class, () -> br.read(nullCharArray, 0, 0)); + assertThrows(NullPointerException.class, () -> br.read(nullCharArray, 0, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> br.read(charArray, -1, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> br.read(charArray, -1, 0)); + + br.read(charArray, 0, 0); + br.read(charArray, 0, charArray.length); + br.read(charArray, charArray.length, 0); + + assertThrows(IndexOutOfBoundsException.class, () -> br.read(charArray, charArray.length + 1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> br.read(charArray, charArray.length + 1, 1)); + + br.close(); + + assertThrows(IOException.class, () -> br.read(nullCharArray, -1, -1)); + assertThrows(IOException.class, () -> br.read(charArray, -1, 0)); + assertThrows(IOException.class, () -> br.read(charArray, 0, -1)); + } + + /** + * Tests {@link UnsynchronizedBufferedReader#read(char[], int, int)}. + * + * @throws IOException test failure. + */ + @Test + public void test_read$CII() throws IOException { + final char[] ca = new char[2]; + try (UnsynchronizedBufferedReader toRet = new UnsynchronizedBufferedReader(new InputStreamReader(new ByteArrayInputStream(new byte[0])))) { + /* Null buffer should throw NPE even when len == 0 */ + assertThrows(NullPointerException.class, () -> toRet.read(null, 1, 0)); + toRet.close(); + assertThrows(IOException.class, () -> toRet.read(null, 1, 0)); + /* Closed reader should throw IOException reading zero bytes */ + assertThrows(IOException.class, () -> toRet.read(ca, 0, 0)); + /* + * Closed reader should throw IOException in preference to index out of bounds + */ + // Read should throw IOException before + // ArrayIndexOutOfBoundException + assertThrows(IOException.class, () -> toRet.read(ca, 1, 5)); + } + // Test to ensure that a drained stream returns 0 at EOF + try (UnsynchronizedBufferedReader toRet2 = new UnsynchronizedBufferedReader(new InputStreamReader(new ByteArrayInputStream(new byte[2])))) { + assertEquals(2, toRet2.read(ca, 0, 2)); + assertEquals(-1, toRet2.read(ca, 0, 2)); + assertEquals(0, toRet2.read(ca, 0, 0)); + } + + // Test for method int UnsynchronizedBufferedReader.read(char [], int, int) + final char[] buf = new char[testString.length()]; + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + br.read(buf, 50, 500); + assertTrue(new String(buf, 50, 500).equals(testString.substring(0, 500))); + + try (UnsynchronizedBufferedReader bufin = new UnsynchronizedBufferedReader(new Reader() { + int size = 2, pos = 0; + + char[] contents = new char[size]; + + @Override + public void close() throws IOException { + } + + @Override + public int read() throws IOException { + if (pos >= size) { + throw new IOException("Read past end of data"); + } + return contents[pos++]; + } + + @Override + public int read(final char[] buf, final int off, final int len) throws IOException { + if (pos >= size) { + throw new IOException("Read past end of data"); + } + int toRead = len; + if (toRead > size - pos) { + toRead = size - pos; + } + System.arraycopy(contents, pos, buf, off, toRead); + pos += toRead; + return toRead; + } + + @Override + public boolean ready() throws IOException { + return size - pos > 0; + } + })) { + bufin.read(); + final int result = bufin.read(new char[2], 0, 2); + assertTrue(result == 1); + } + // regression for HARMONY-831 + try (Reader reader = new UnsynchronizedBufferedReader(new PipedReader(), 9)) { + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(new char[] {}, 7, 0)); + } + + // Regression for HARMONY-54 + final char[] ch = {}; + @SuppressWarnings("resource") + final UnsynchronizedBufferedReader reader = new UnsynchronizedBufferedReader(new CharArrayReader(ch)); + // Check exception thrown when the reader is open. + assertThrows(NullPointerException.class, () -> reader.read(null, 1, 0)); + + // Now check IOException is thrown in preference to + // NullPointerexception when the reader is closed. + reader.close(); + assertThrows(IOException.class, () -> reader.read(null, 1, 0)); + + // And check that the IOException is thrown before + // ArrayIndexOutOfBoundException + assertThrows(IOException.class, () -> reader.read(ch, 0, 42)); + } + + /** + * Tests {@link UnsynchronizedBufferedReader#readLine()}. + * + * @throws IOException test failure. + */ + @Test + public void test_readLine() throws IOException { + // Test for method java.lang.String UnsynchronizedBufferedReader.readLine() + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + final String r = br.readLine(); + assertEquals("Test_All_Tests", r); + } + + /** + * The spec says that BufferedReader.readLine() considers only "\r", "\n" and "\r\n" to be line separators. We must not permit additional separator + * characters. + * + * @throws IOException test failure. + */ + @Test + public void test_readLine_IgnoresEbcdic85Characters() throws IOException { + assertLines("A\u0085B", "A\u0085B"); + } + + @Test + public void test_readLine_Separators() throws IOException { + assertLines("A\nB\nC", "A", "B", "C"); + assertLines("A\rB\rC", "A", "B", "C"); + assertLines("A\r\nB\r\nC", "A", "B", "C"); + assertLines("A\n\rB\n\rC", "A", "", "B", "", "C"); + assertLines("A\n\nB\n\nC", "A", "", "B", "", "C"); + assertLines("A\r\rB\r\rC", "A", "", "B", "", "C"); + assertLines("A\n\n", "A", ""); + assertLines("A\n\r", "A", ""); + assertLines("A\r\r", "A", ""); + assertLines("A\r\n", "A"); + assertLines("A\r\n\r\n", "A", ""); + } + + /** + * Tests {@link UnsynchronizedBufferedReader#ready()}. + * + * @throws IOException test failure. + */ + @Test + public void test_ready() throws IOException { + // Test for method boolean UnsynchronizedBufferedReader.ready() + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + assertTrue(br.ready()); + } + + /** + * Tests {@link UnsynchronizedBufferedReader#reset()}. + * + * @throws IOException + */ + @Test + public void test_reset() throws IOException { + // Test for method void UnsynchronizedBufferedReader.reset() + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + br.skip(500); + br.mark(900); + br.skip(500); + br.reset(); + final char[] buf = new char[testString.length()]; + br.read(buf, 0, 500); + assertTrue(testString.substring(500, 1000).equals(new String(buf, 0, 500))); + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + br.skip(500); + assertThrows(IOException.class, br::reset); + } + + @Test + public void test_reset_IOException() throws Exception { + final int[] expected = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', -1 }; + br = new UnsynchronizedBufferedReader(new StringReader("1234567890"), 9); + br.mark(9); + for (int i = 0; i < 11; i++) { + assertEquals(expected[i], br.read()); + } + assertThrows(IOException.class, br::reset); + for (int i = 0; i < 11; i++) { + assertEquals(-1, br.read()); + } + + br = new UnsynchronizedBufferedReader(new StringReader("1234567890")); + br.mark(10); + for (int i = 0; i < 10; i++) { + assertEquals(expected[i], br.read()); + } + br.reset(); + for (int i = 0; i < 11; i++) { + assertEquals(expected[i], br.read()); + } + } + + /** + * Tests {@link UnsynchronizedBufferedReader#skip(long)}. + * + * @throws IOException + */ + @Test + public void test_skipJ() throws IOException { + // Test for method long UnsynchronizedBufferedReader.skip(long) + br = new UnsynchronizedBufferedReader(new StringReader(testString)); + br.skip(500); + final char[] buf = new char[testString.length()]; + br.read(buf, 0, 500); + assertTrue(testString.substring(500, 1000).equals(new String(buf, 0, 500))); + } +}