diff --git a/src/java.base/share/classes/java/io/Reader.java b/src/java.base/share/classes/java/io/Reader.java index 5dbe4ab5fae..9fca28a3a96 100644 --- a/src/java.base/share/classes/java/io/Reader.java +++ b/src/java.base/share/classes/java/io/Reader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -140,6 +140,119 @@ public abstract class Reader implements Readable, Closeable { }; } + /** + * Returns a {@code Reader} that reads characters from a + * {@code CharSequence}. The reader is initially open and reading starts at + * the first character in the sequence. + * + *

The returned reader supports the {@link #mark mark()} and + * {@link #reset reset()} operations. + * + *

The resulting reader is not safe for use by multiple + * concurrent threads. If the reader is to be used by more than one + * thread it should be controlled by appropriate synchronization. + * + *

If the sequence changes while the reader is open, e.g. the length + * changes, the behavior is undefined. + * + * @param cs {@code CharSequence} providing the character stream. + * @return a {@code Reader} which reads characters from {@code cs} + * @throws NullPointerException if {@code cs} is {@code null} + * + * @since 24 + */ + public static Reader of(final CharSequence cs) { + Objects.requireNonNull(cs); + + return new Reader() { + private boolean isClosed; + private int next = 0; + private int mark = 0; + + /** Check to make sure that the stream has not been closed */ + private void ensureOpen() throws IOException { + if (isClosed) + throw new IOException("Stream closed"); + } + + @Override + public int read() throws IOException { + ensureOpen(); + if (next >= cs.length()) + return -1; + return cs.charAt(next++); + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + ensureOpen(); + Objects.checkFromIndexSize(off, len, cbuf.length); + if (len == 0) { + return 0; + } + int length = cs.length(); + if (next >= length) + return -1; + int n = Math.min(length - next, len); + switch (cs) { + case String s -> s.getChars(next, next + n, cbuf, off); + case StringBuilder sb -> sb.getChars(next, next + n, cbuf, off); + case StringBuffer sb -> sb.getChars(next, next + n, cbuf, off); + case CharBuffer cb -> cb.get(next, cbuf, off, n); + default -> { + for (int i = 0; i < n; i++) + cbuf[off + i] = cs.charAt(next + i); + } + } + next += n; + return n; + } + + @Override + public long skip(long n) throws IOException { + ensureOpen(); + if (next >= cs.length()) + return 0; + // Bound skip by beginning and end of the source + long r = Math.min(cs.length() - next, n); + r = Math.max(-next, r); + next += (int)r; + return r; + } + + @Override + public boolean ready() throws IOException { + ensureOpen(); + return true; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void mark(int readAheadLimit) throws IOException { + if (readAheadLimit < 0){ + throw new IllegalArgumentException("Read-ahead limit < 0"); + } + ensureOpen(); + mark = next; + } + + @Override + public void reset() throws IOException { + ensureOpen(); + next = mark; + } + + @Override + public void close() { + isClosed = true; + } + }; + } + /** * The object used to synchronize operations on this stream. For * efficiency, a character-stream object may use an object other than diff --git a/src/java.base/share/classes/java/io/StringReader.java b/src/java.base/share/classes/java/io/StringReader.java index 2023aadf6c8..719e3246adf 100644 --- a/src/java.base/share/classes/java/io/StringReader.java +++ b/src/java.base/share/classes/java/io/StringReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,16 +30,17 @@ import java.util.Objects; /** * A character stream whose source is a string. * + * @apiNote + * {@link Reader#of(CharSequence)} provides a method to read from any + * {@link CharSequence} that may be more efficient than {@code StringReader}. + * * @author Mark Reinhold * @since 1.1 */ public class StringReader extends Reader { - private final int length; - private String str; - private int next = 0; - private int mark = 0; + private final Reader r; /** * Creates a new string reader. @@ -47,14 +48,7 @@ public class StringReader extends Reader { * @param s String providing the character stream. */ public StringReader(String s) { - this.length = s.length(); - this.str = s; - } - - /** Check to make sure that the stream has not been closed */ - private void ensureOpen() throws IOException { - if (str == null) - throw new IOException("Stream closed"); + r = Reader.of(s); } /** @@ -67,10 +61,7 @@ public class StringReader extends Reader { */ public int read() throws IOException { synchronized (lock) { - ensureOpen(); - if (next >= length) - return -1; - return str.charAt(next++); + return r.read(); } } @@ -94,17 +85,7 @@ public class StringReader extends Reader { */ public int read(char[] cbuf, int off, int len) throws IOException { synchronized (lock) { - ensureOpen(); - Objects.checkFromIndexSize(off, len, cbuf.length); - if (len == 0) { - return 0; - } - if (next >= length) - return -1; - int n = Math.min(length - next, len); - str.getChars(next, next + n, cbuf, off); - next += n; - return n; + return r.read(cbuf, off, len); } } @@ -130,14 +111,7 @@ public class StringReader extends Reader { */ public long skip(long n) throws IOException { synchronized (lock) { - ensureOpen(); - if (next >= length) - return 0; - // Bound skip by beginning and end of the source - long r = Math.min(length - next, n); - r = Math.max(-next, r); - next += (int)r; - return r; + return r.skip(n); } } @@ -150,8 +124,7 @@ public class StringReader extends Reader { */ public boolean ready() throws IOException { synchronized (lock) { - ensureOpen(); - return true; + return r.ready(); } } @@ -176,12 +149,8 @@ public class StringReader extends Reader { * @throws IOException If an I/O error occurs */ public void mark(int readAheadLimit) throws IOException { - if (readAheadLimit < 0){ - throw new IllegalArgumentException("Read-ahead limit < 0"); - } synchronized (lock) { - ensureOpen(); - mark = next; + r.mark(readAheadLimit); } } @@ -193,8 +162,7 @@ public class StringReader extends Reader { */ public void reset() throws IOException { synchronized (lock) { - ensureOpen(); - next = mark; + r.reset(); } } @@ -207,7 +175,11 @@ public class StringReader extends Reader { */ public void close() { synchronized (lock) { - str = null; + try { + r.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } } } diff --git a/test/jdk/java/io/Reader/Of.java b/test/jdk/java/io/Reader/Of.java new file mode 100644 index 00000000000..491c0499e6b --- /dev/null +++ b/test/jdk/java/io/Reader/Of.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.Reader; +import java.io.StringReader; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.ReadOnlyBufferException; + +import org.testng.annotations.*; + +import static org.testng.Assert.*; + +/* + * @test + * @bug 8341566 + * @summary Check for expected behavior of Reader.of(). + * @run testng Of + */ +public class Of { + final static String CONTENT = "Some Reader Test"; + + /* + * Readers to be tested. + */ + @DataProvider + public static Reader[] readers() { + return new Reader[] { + new StringReader(CONTENT), + Reader.of(CONTENT), + Reader.of(new StringBuffer(CONTENT)), + Reader.of(new StringBuilder(CONTENT)), + Reader.of(ByteBuffer.allocateDirect(CONTENT.length() * 2) + .asCharBuffer().put(CONTENT).flip()), + Reader.of(CharBuffer.wrap(CONTENT.toCharArray())), + Reader.of(new CharSequence() { + @Override + public char charAt(int index) { + return CONTENT.charAt(index); + } + + @Override + public int length() { + return CONTENT.length(); + } + + @Override + public CharSequence subSequence(int start, int end) { + // unused by Reader.Of's result + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + // Reader.Of's result SHALL NOT convert to String + throw new UnsupportedOperationException(); + } + }) + }; + } + + @Test(dataProvider = "readers") + public void testRead(Reader reader) throws IOException { + String s = ""; + for (int c; (c = reader.read()) != -1; s += (char) c); + assertEquals(s, CONTENT, "read() returned wrong value"); + } + + @Test(dataProvider = "readers") + public void testReadBII(Reader reader) throws IOException { + char[] c = new char[16]; + assertEquals(reader.read(c, 8, 8), 8, + "read(char[],int,int) does not respect given start or end"); + assertEquals(reader.read(c, 0, 16), 8, + "read(char[],int,int) does not respect end of stream"); + assertEquals(new String(c), + CONTENT.substring(8, 16) + CONTENT.substring(0, 8), + "read(char[],int,int) provides wrong content"); + } + + @Test(dataProvider = "readers") + public void testReadBIILenZero(Reader reader) throws IOException { + assertEquals(reader.read(new char[1], 0, 0), 0, + "read(char[],int,int) != 0"); + } + + @Test(dataProvider = "readers") + public void testReadDirectCharBuffer(Reader reader) throws IOException { + CharBuffer charBuffer = ByteBuffer.allocateDirect(32).asCharBuffer(); + charBuffer.position(8); + assertEquals(reader.read(charBuffer), 8, + "read(CharBuffer) does not respect position or limit"); + charBuffer.rewind(); + assertEquals(reader.read(charBuffer), 8, + "read(CharBuffer) does not respect end of stream"); + charBuffer.rewind(); + assertEquals(charBuffer.toString(), + // last part first proofs that copy loops correctly stopped + CONTENT.substring(8, 16) + CONTENT.substring(0, 8), + "read(CharBuffer) provides wrong content"); + } + + @Test(dataProvider = "readers") + public void testReadNonDirectCharBuffer(Reader reader) throws IOException { + CharBuffer charBuffer = CharBuffer.allocate(16); + charBuffer.position(8); + assertEquals(reader.read(charBuffer), 8, + "read(CharBuffer) does not respect position or limit"); + charBuffer.rewind(); + assertEquals(reader.read(charBuffer), 8, + "read(CharBuffer) does not respect end of stream"); + charBuffer.rewind(); + assertEquals(charBuffer.toString(), + CONTENT.substring(8, 16) + CONTENT.substring(0, 8), + "read(CharBuffer) provides wrong content"); + } + + @Test(dataProvider = "readers") + public void testReadCharBufferZeroRemaining(Reader reader) throws IOException { + CharBuffer charBuffer = CharBuffer.allocate(0); + assertEquals(reader.read(charBuffer), 0, "read(CharBuffer) != 0"); + } + + @Test(dataProvider = "readers") + public void testReady(Reader reader) throws IOException { + assertTrue(reader.ready()); + } + + @Test(dataProvider = "readers") + public void testSkip(Reader reader) throws IOException { + assertEquals(reader.skip(8), 8, "skip() does not respect limit"); + assertEquals(reader.skip(9), 8, "skip() does not respect end of stream"); + assertEquals(reader.skip(1), 0, "skip() does not respect empty stream"); + } + + @Test(dataProvider = "readers") + public void testTransferTo(Reader reader) throws IOException { + StringWriter sw = new StringWriter(16); + assertEquals(reader.transferTo(sw), 16, "transferTo() != 16"); + assertEquals(reader.transferTo(sw), 0, + "transferTo() does not respect empty stream"); + assertEquals(sw.toString(), CONTENT, + "transferTo() provides wrong content"); + } + + @Test(dataProvider = "readers") + public void testReadClosed(Reader reader) throws IOException { + reader.close(); + assertThrows(IOException.class, () -> {reader.read();}); + } + + @Test(dataProvider = "readers") + public void testReadBIIClosed(Reader reader) throws IOException { + reader.close(); + assertThrows(IOException.class, () -> reader.read(new char[1], 0, 1)); + } + + @Test(dataProvider = "readers") + public void testReadCharBufferClosed(Reader reader) throws IOException { + CharBuffer charBuffer = CharBuffer.allocate(1); + reader.close(); + assertThrows(IOException.class, () -> reader.read(charBuffer)); + } + + @Test(dataProvider = "readers") + public void testReadCharBufferZeroRemainingClosed(Reader reader) throws IOException { + CharBuffer charBuffer = CharBuffer.allocate(0); + reader.close(); + assertThrows(IOException.class, () -> reader.read(charBuffer)); + } + + @Test(dataProvider = "readers") + public void testReadyClosed(Reader reader) throws IOException { + reader.close(); + assertThrows(IOException.class, () -> reader.ready()); + } + + @Test(dataProvider = "readers") + public void testSkipClosed(Reader reader) throws IOException { + reader.close(); + assertThrows(IOException.class, () -> reader.skip(1)); + } + + @Test(dataProvider = "readers") + public void testTransferToClosed(Reader reader) throws IOException { + reader.close(); + assertThrows(IOException.class, () -> reader.transferTo(new StringWriter(1))); + } + + @Test(dataProvider = "readers") + public void testCloseClosed(Reader reader) throws IOException { + reader.close(); + reader.close(); + } +}