From ba6d14555ac805fdc5d39781e34744f3c4f27e10 Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Wed, 18 Dec 2024 17:09:02 +0000 Subject: [PATCH 01/10] Writer.of(Appendable) --- .../share/classes/java/io/StringWriter.java | 53 ++++++-- .../share/classes/java/io/Writer.java | 113 ++++++++++++++++++ 2 files changed, 155 insertions(+), 11 deletions(-) diff --git a/src/java.base/share/classes/java/io/StringWriter.java b/src/java.base/share/classes/java/io/StringWriter.java index ee58d741cc869..150c94d22c5b0 100644 --- a/src/java.base/share/classes/java/io/StringWriter.java +++ b/src/java.base/share/classes/java/io/StringWriter.java @@ -36,6 +36,10 @@ * can be called after the stream has been closed without generating an * {@code IOException}. * + * @apiNote + * {@link Writer#of(Appendable)} provides a method to write into any + * {@link Appendable} that may be more efficient than {@code StringWriter}. + * * @author Mark Reinhold * @since 1.1 */ @@ -43,6 +47,7 @@ public class StringWriter extends Writer { private final StringBuffer buf; + private final Writer w; /** * Create a new string writer using the default initial string-buffer @@ -51,6 +56,7 @@ public class StringWriter extends Writer { public StringWriter() { buf = new StringBuffer(); lock = buf; + w = Writer.of(buf); } /** @@ -70,13 +76,18 @@ public StringWriter(int initialSize) { } buf = new StringBuffer(initialSize); lock = buf; + w = Writer.of(buf); } /** * Write a single character. */ public void write(int c) { - buf.append((char) c); + try { + w.write(c); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } /** @@ -92,18 +103,22 @@ public void write(int c) { * of the given array */ public void write(char[] cbuf, int off, int len) { - Objects.checkFromIndexSize(off, len, cbuf.length); - if (len == 0) { - return; + try { + w.write(cbuf, off, len); + } catch (IOException e) { + throw new UncheckedIOException(e); } - buf.append(cbuf, off, len); } /** * Write a string. */ public void write(String str) { - buf.append(str); + try { + w.write(str); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } /** @@ -119,7 +134,11 @@ public void write(String str) { * of the given string */ public void write(String str, int off, int len) { - buf.append(str, off, off + len); + try { + w.write(str, off, len); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } /** @@ -149,7 +168,11 @@ public void write(String str, int off, int len) { * @since 1.5 */ public StringWriter append(CharSequence csq) { - write(String.valueOf(csq)); + try { + w.append(csq); + } catch (IOException e) { + throw new UncheckedIOException(e); + } return this; } @@ -188,8 +211,12 @@ public StringWriter append(CharSequence csq) { * @since 1.5 */ public StringWriter append(CharSequence csq, int start, int end) { - if (csq == null) csq = "null"; - return append(csq.subSequence(start, end)); + try { + w.append(csq, start, end); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; } /** @@ -210,7 +237,11 @@ public StringWriter append(CharSequence csq, int start, int end) { * @since 1.5 */ public StringWriter append(char c) { - write(c); + try { + w.append(c); + } catch (IOException e) { + throw new UncheckedIOException(e); + } return this; } diff --git a/src/java.base/share/classes/java/io/Writer.java b/src/java.base/share/classes/java/io/Writer.java index 433a116a4bbcd..4344e4081f623 100644 --- a/src/java.base/share/classes/java/io/Writer.java +++ b/src/java.base/share/classes/java/io/Writer.java @@ -25,6 +25,7 @@ package java.io; +import java.nio.CharBuffer; import java.util.Objects; /** @@ -144,6 +145,118 @@ public void close() throws IOException { }; } + /** + * Returns a {@code Writer} that writes characters into an + * {@code Appendable}. The writer is initially open and writing appends + * after the last character in the builder. + * + *

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

If the appendable changes while the writer is open, e.g. the length + * changes, the behavior is undefined. + * + * @param a {@code Appendable} consuming the character stream. + * @return a {@code Writer} which writes characters into {@code a} + * @throws NullPointerException if {@code a} is {@code null} + * + * @since 25 + */ + public static Writer of(final Appendable a) { + Objects.requireNonNull(a); + + return new Writer() { + private boolean isClosed; + + /** 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 void write(int c) throws IOException { + ensureOpen(); + switch (a) { + case Writer w -> w.write(c); + default -> a.append((char) c); + } + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + ensureOpen(); + Objects.checkFromIndexSize(off, len, cbuf.length); + if (len == 0) { + return; + } + switch (a) { + case StringBuilder sb -> sb.append(cbuf, off, len); + case StringBuffer sb -> sb.append(cbuf, off, len); + case CharBuffer cb -> cb.put(cbuf, off, len); + case Writer w -> w.write(cbuf, off, len); + default -> { + for (int i = 0; i < len; i++) + a.append(cbuf[off + i]); + } + } + } + + @Override + public void write(String str) throws IOException { + ensureOpen(); + switch (a) { + case StringBuilder sb -> sb.append(str); + case StringBuffer sb -> sb.append(str); + case CharBuffer cb -> cb.put(str); + case Writer w -> w.write(str); + default -> a.append(str); + } + } + + @Override + public void write(String str, int off, int len) throws IOException { + ensureOpen(); + switch (a) { + case Writer w -> w.write(str, off, len); + default -> a.append(str, off, off + len); + } + } + + @Override + public Writer append(CharSequence csq) throws IOException { + ensureOpen(); + a.append(csq); + return this; + } + + @Override + public Writer append(CharSequence csq, int start, int end) throws IOException { + ensureOpen(); + a.append(csq, start, end); + return this; + } + + @Override + public Writer append(char c) throws IOException { + ensureOpen(); + a.append(c); + return this; + } + + @Override + public void flush() throws IOException { + ensureOpen(); + } + + @Override + public void close() throws IOException { + isClosed = true; + } + }; + } + /** * The object used to synchronize operations on this stream. For * efficiency, a character-stream object may use an object other than From af380318486c262c9f32274c8fa223278f34396a Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Thu, 19 Dec 2024 13:02:23 +0000 Subject: [PATCH 02/10] Draft: Test for Writer.of(Appendable) --- test/jdk/java/io/Writer/Of.java | 321 ++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 test/jdk/java/io/Writer/Of.java diff --git a/test/jdk/java/io/Writer/Of.java b/test/jdk/java/io/Writer/Of.java new file mode 100644 index 0000000000000..685161af94b0f --- /dev/null +++ b/test/jdk/java/io/Writer/Of.java @@ -0,0 +1,321 @@ +/* + * 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.Writer; +import java.io.StringWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.ReadOnlyBufferException; +import java.util.function.Supplier; + +import org.testng.annotations.*; + +import static org.testng.Assert.*; + +/* + * @test + * @bug 1234567 + * @summary Check for expected behavior of Writer.of(). + * @run testng Of + */ +public class Of { + private static final String CONTENT = "Some Writer Test"; + + private static record Config(String id, Writer writer, Supplier spy) { + @Override + public String toString() { + return id; // allows to identify config when test case fails + } + }; + + /* + * Writers to be tested. + */ + @DataProvider + public static Config[] writers() { + var sw = new StringWriter(); + var sbuf = new StringBuffer(); + var sbld = new StringBuilder(); + var dcb = ByteBuffer.allocateDirect(CONTENT.length() * 2).asCharBuffer(); + var wcb = CharBuffer.wrap(new char[CONTENT.length()]); + var w = new Writer() { + private String s = ""; + private boolean isClosed; + + private void ensureOpen() throws IOException { + if (isClosed) + throw new IOException("Stream closed"); + } + + @Override + public Writer append(char c) throws IOException { + ensureOpen(); + s += c; + return this; + } + + @Override + public Writer append(CharSequence csq) throws IOException { + ensureOpen(); + s += String.valueOf(csq); + return this; + } + + @Override + public Writer append(CharSequence csq, int start, int end) + throws IOException { + ensureOpen(); + s += String.valueOf(csq).subSequence(start, end); + return this; + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + ensureOpen(); + s += new String(cbuf, off, len); + } + + @Override + public void flush() throws IOException { + ensureOpen(); + } + + @Override + public void close() throws IOException { + isClosed = true; + } + + @Override + public String toString() { + return s; + } + }; + var a = new Appendable() { + private String s = ""; + + @Override + public Appendable append(char c) throws IOException { + s += c; + return this; + } + + @Override + public Appendable append(CharSequence csq) throws IOException { + s += String.valueOf(csq); + return this; + } + + @Override + public Appendable append(CharSequence csq, int start, int end) + throws IOException { + s += String.valueOf(csq).subSequence(start, end); + return this; + } + + @Override + public String toString() { + return s; + } + }; + return new Config[] { + new Config("StringWriter", sw, sw::toString), + new Config("StringBuffer", Writer.of(sbuf), sbuf::toString), + new Config("StringBuilder", Writer.of(sbld), sbld::toString), + new Config("Direct CharBuffer", Writer.of(dcb), + () -> { dcb.flip(); return dcb.toString(); }), + new Config("Wrapped CharBuffer", Writer.of(wcb), + () -> { wcb.flip(); return wcb.toString(); }), + new Config("Custom Writer", w, w::toString), + new Config("Custom Appendable", Writer.of(a), a::toString) + }; + } + + @Test(dataProvider = "writers") + public void testAppendChar(Config config) throws IOException { + for (int i = 0; i < CONTENT.length(); i++) + config.writer.append(CONTENT.charAt(i)); + assertEquals(config.spy.get(), CONTENT); + } + + @Test(dataProvider = "writers") + public void testAppendCharSequence(Config config) throws IOException { + config.writer.append((CharSequence) CONTENT); + assertEquals(config.spy.get(), CONTENT); + } + + @Test(dataProvider = "writers") + public void testAppendCompleteSubCharSequence(Config config) throws IOException { + config.writer.append((CharSequence) CONTENT, 0, CONTENT.length()); + assertEquals(config.spy.get(), CONTENT); + } + + @Test(dataProvider = "writers") + public void testAppendPartialSubCharSequence(Config config) throws IOException { + config.writer.append((CharSequence) CONTENT, 1, CONTENT.length() - 1); + assertEquals(config.spy.get(), CONTENT.substring(1, CONTENT.length() - 1)); + } + + @Test(dataProvider = "writers") + public void testWriteCharArray(Config config) throws IOException { + config.writer.write(CONTENT.toCharArray()); + assertEquals(config.spy.get(), CONTENT); + } + + @Test(dataProvider = "writers") + public void testWriteCompleteSubCharArray(Config config) throws IOException { + config.writer.write(CONTENT.toCharArray(), 0, CONTENT.length()); + assertEquals(config.spy.get(), CONTENT); + } + + @Test(dataProvider = "writers") + public void testWritePartialSubCharArray(Config config) throws IOException { + config.writer.write(CONTENT.toCharArray(), 1, CONTENT.length() - 2); + assertEquals(config.spy.get(), CONTENT.substring(1, CONTENT.length() - 1)); + } + + @Test(dataProvider = "writers") + public void testWriteChar(Config config) throws IOException { + for (int i = 0; i < CONTENT.length(); i++) + config.writer.write(CONTENT.charAt(i)); + assertEquals(config.spy.get(), CONTENT); +} + + @Test(dataProvider = "writers") + public void testWriteString(Config config) throws IOException { + config.writer.write(CONTENT); + assertEquals(config.spy.get(), CONTENT); + } + + @Test(dataProvider = "writers") + public void testWriteCompleteSubString(Config config) throws IOException { + config.writer.write(CONTENT, 0, CONTENT.length()); + assertEquals(config.spy.get(), CONTENT); + } + + @Test(dataProvider = "writers") + public void testWritePartialSubString(Config config) throws IOException { + config.writer.write(CONTENT, 1, CONTENT.length() - 2); + assertEquals(config.spy.get(), CONTENT.substring(1, CONTENT.length() - 1)); + } + + @Test(dataProvider = "writers") + public void testAppendCharClosed(Config config) throws IOException { + config.writer.close(); + + // StringWriter intentionally never throws exceptions + if (config.writer instanceof StringWriter) + testAppendChar(config); + else + assertThrows(IOException.class, () -> config.writer.append('x')); + } + + @Test(dataProvider = "writers") + public void testAppendCharSequenceClosed(Config config) throws IOException { + config.writer.close(); + + // StringWriter intentionally never throws exceptions + if (config.writer instanceof StringWriter) + testAppendCharSequence(config); + else + assertThrows(IOException.class, () -> + config.writer.append((CharSequence) CONTENT)); + } + + @Test(dataProvider = "writers") + public void testAppendSubCharSequenceClosed(Config config) throws IOException { + config.writer.close(); + + // StringWriter intentionally never throws exceptions + if (config.writer instanceof StringWriter) + testAppendCompleteSubCharSequence(config); + else + assertThrows(IOException.class, () -> + config.writer.append((CharSequence) CONTENT, 0, CONTENT.length())); + } + + @Test(dataProvider = "writers") + public void testWriteCharArrayClosed(Config config) throws IOException { + config.writer.close(); + + // StringWriter intentionally never throws exceptions + if (config.writer instanceof StringWriter) + testWriteCharArray(config); + else + assertThrows(IOException.class, () -> + config.writer.write(CONTENT.toCharArray())); + } + + @Test(dataProvider = "writers") + public void testWriteSubCharArrayClosed(Config config) throws IOException { + config.writer.close(); + + // StringWriter intentionally never throws exceptions + if (config.writer instanceof StringWriter) + testWriteCompleteSubCharArray(config); + else + assertThrows(IOException.class, () -> + config.writer.write(CONTENT.toCharArray(), 0, CONTENT.length())); + } + + @Test(dataProvider = "writers") + public void testWriteCharClosed(Config config) throws IOException { + config.writer.close(); + + // StringWriter intentionally never throws exceptions + if (config.writer instanceof StringWriter) + testWriteChar(config); + else + assertThrows(IOException.class, () -> config.writer.write('x')); + } + + @Test(dataProvider = "writers") + public void testWriteStringClosed(Config config) throws IOException { + config.writer.close(); + + // StringWriter intentionally never throws exceptions + if (config.writer instanceof StringWriter) + testWriteString(config); + else + assertThrows(IOException.class, () -> config.writer.write(CONTENT)); + } + + @Test(dataProvider = "writers") + public void testWriteSubStringClosed(Config config) throws IOException { + config.writer.close(); + + // StringWriter intentionally never throws exceptions + if (config.writer instanceof StringWriter) + testWriteCompleteSubString(config); + else + assertThrows(IOException.class, () -> + config.writer.write(CONTENT, 0, CONTENT.length())); + } + + @Test(dataProvider = "writers") + public void testClosedClosed(Config config) throws IOException { + config.writer.close(); + config.writer.close(); + } +} From c7ac2cab45d31193ddcde5341c28863dd7817266 Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Sat, 28 Dec 2024 15:07:36 +0000 Subject: [PATCH 03/10] Forward flush() and close() for Flushable and Closeable --- src/java.base/share/classes/java/io/Writer.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/java.base/share/classes/java/io/Writer.java b/src/java.base/share/classes/java/io/Writer.java index 4344e4081f623..438c42518ab22 100644 --- a/src/java.base/share/classes/java/io/Writer.java +++ b/src/java.base/share/classes/java/io/Writer.java @@ -248,10 +248,21 @@ public Writer append(char c) throws IOException { @Override public void flush() throws IOException { ensureOpen(); + implFlush(); + } + + private void implFlush() throws IOException { + if (a instanceof Flushable f) + f.flush(); } @Override public void close() throws IOException { + if (isClosed) + return; + implFlush(); + if (a instanceof Closable c) + c.close(); isClosed = true; } }; From d243cf84b457852ef8e286cb81e14cd5535ada06 Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Sat, 28 Dec 2024 15:13:56 +0000 Subject: [PATCH 04/10] Do not wrap Writer, but return it as-is --- .../share/classes/java/io/Writer.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/java.base/share/classes/java/io/Writer.java b/src/java.base/share/classes/java/io/Writer.java index 438c42518ab22..e80c8a941ba88 100644 --- a/src/java.base/share/classes/java/io/Writer.java +++ b/src/java.base/share/classes/java/io/Writer.java @@ -150,6 +150,8 @@ public void close() throws IOException { * {@code Appendable}. The writer is initially open and writing appends * after the last character in the builder. * + *

If the appendable is a {code Writer}, it is returned. + * *

The resulting writer is not safe for use by multiple * concurrent threads. If the writer is to be used by more than one * thread it should be controlled by appropriate synchronization. @@ -158,7 +160,8 @@ public void close() throws IOException { * changes, the behavior is undefined. * * @param a {@code Appendable} consuming the character stream. - * @return a {@code Writer} which writes characters into {@code a} + * @return a {@code Writer} which writes characters into {@code a}, or + * {@code a} if it is a {@code Writer}. * @throws NullPointerException if {@code a} is {@code null} * * @since 25 @@ -166,6 +169,9 @@ public void close() throws IOException { public static Writer of(final Appendable a) { Objects.requireNonNull(a); + if (a instanceof Writer w) + return w; + return new Writer() { private boolean isClosed; @@ -178,10 +184,7 @@ private void ensureOpen() throws IOException { @Override public void write(int c) throws IOException { ensureOpen(); - switch (a) { - case Writer w -> w.write(c); - default -> a.append((char) c); - } + a.append((char) c); } @Override @@ -195,7 +198,6 @@ public void write(char[] cbuf, int off, int len) throws IOException { case StringBuilder sb -> sb.append(cbuf, off, len); case StringBuffer sb -> sb.append(cbuf, off, len); case CharBuffer cb -> cb.put(cbuf, off, len); - case Writer w -> w.write(cbuf, off, len); default -> { for (int i = 0; i < len; i++) a.append(cbuf[off + i]); @@ -210,7 +212,6 @@ public void write(String str) throws IOException { case StringBuilder sb -> sb.append(str); case StringBuffer sb -> sb.append(str); case CharBuffer cb -> cb.put(str); - case Writer w -> w.write(str); default -> a.append(str); } } @@ -218,10 +219,7 @@ public void write(String str) throws IOException { @Override public void write(String str, int off, int len) throws IOException { ensureOpen(); - switch (a) { - case Writer w -> w.write(str, off, len); - default -> a.append(str, off, off + len); - } + a.append(str, off, off + len); } @Override From 6e872ebb67407d7220024b1c74d339890702a54e Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Sat, 8 Feb 2025 23:01:00 +0000 Subject: [PATCH 05/10] Fixed typo --- src/java.base/share/classes/java/io/Writer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/java.base/share/classes/java/io/Writer.java b/src/java.base/share/classes/java/io/Writer.java index e80c8a941ba88..e023458c09931 100644 --- a/src/java.base/share/classes/java/io/Writer.java +++ b/src/java.base/share/classes/java/io/Writer.java @@ -148,7 +148,7 @@ public void close() throws IOException { /** * Returns a {@code Writer} that writes characters into an * {@code Appendable}. The writer is initially open and writing appends - * after the last character in the builder. + * after the last character in the appendable. * *

If the appendable is a {code Writer}, it is returned. * From 1bb8ac0286b386f5222eb4d8bbcb657acfd70439 Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Sun, 9 Feb 2025 10:24:27 +0000 Subject: [PATCH 06/10] Must not change implementation of StringWriter as getBuffer would not reflect current content of internally used Writer --- .../share/classes/java/io/StringWriter.java | 49 +++++-------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/src/java.base/share/classes/java/io/StringWriter.java b/src/java.base/share/classes/java/io/StringWriter.java index 150c94d22c5b0..66e4225e03c3d 100644 --- a/src/java.base/share/classes/java/io/StringWriter.java +++ b/src/java.base/share/classes/java/io/StringWriter.java @@ -47,7 +47,6 @@ public class StringWriter extends Writer { private final StringBuffer buf; - private final Writer w; /** * Create a new string writer using the default initial string-buffer @@ -56,7 +55,6 @@ public class StringWriter extends Writer { public StringWriter() { buf = new StringBuffer(); lock = buf; - w = Writer.of(buf); } /** @@ -76,18 +74,13 @@ public StringWriter(int initialSize) { } buf = new StringBuffer(initialSize); lock = buf; - w = Writer.of(buf); } /** * Write a single character. */ public void write(int c) { - try { - w.write(c); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + buf.append((char) c); } /** @@ -103,22 +96,18 @@ public void write(int c) { * of the given array */ public void write(char[] cbuf, int off, int len) { - try { - w.write(cbuf, off, len); - } catch (IOException e) { - throw new UncheckedIOException(e); + Objects.checkFromIndexSize(off, len, cbuf.length); + if (len == 0) { + return; } + buf.append(cbuf, off, len); } /** * Write a string. */ public void write(String str) { - try { - w.write(str); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + buf.append(str); } /** @@ -134,11 +123,7 @@ public void write(String str) { * of the given string */ public void write(String str, int off, int len) { - try { - w.write(str, off, len); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + buf.append(str, off, off + len); } /** @@ -168,11 +153,7 @@ public void write(String str, int off, int len) { * @since 1.5 */ public StringWriter append(CharSequence csq) { - try { - w.append(csq); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + write(String.valueOf(csq)); return this; } @@ -211,12 +192,8 @@ public StringWriter append(CharSequence csq) { * @since 1.5 */ public StringWriter append(CharSequence csq, int start, int end) { - try { - w.append(csq, start, end); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return this; + if (csq == null) csq = "null"; + return append(csq.subSequence(start, end)); } /** @@ -237,11 +214,7 @@ public StringWriter append(CharSequence csq, int start, int end) { * @since 1.5 */ public StringWriter append(char c) { - try { - w.append(c); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + write(c); return this; } From bf564cb50860543ce4e8f8d4c9348db178c560bd Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Sun, 23 Mar 2025 18:18:13 +0000 Subject: [PATCH 07/10] Limited to Writer.of(StringBuilder) --- .../share/classes/java/io/StringWriter.java | 6 +- .../share/classes/java/io/Writer.java | 64 +++++-------------- .../classes/java/nio/X-Buffer.java.template | 2 +- test/jdk/java/io/Writer/Of.java | 44 +------------ 4 files changed, 23 insertions(+), 93 deletions(-) diff --git a/src/java.base/share/classes/java/io/StringWriter.java b/src/java.base/share/classes/java/io/StringWriter.java index 66e4225e03c3d..8edd0145b2478 100644 --- a/src/java.base/share/classes/java/io/StringWriter.java +++ b/src/java.base/share/classes/java/io/StringWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -37,8 +37,8 @@ * {@code IOException}. * * @apiNote - * {@link Writer#of(Appendable)} provides a method to write into any - * {@link Appendable} that may be more efficient than {@code StringWriter}. + * {@link Writer#of(StringBuilder)} provides a method to write into an existing + * {@link StringBuilder} that may be more efficient than {@code StringWriter}. * * @author Mark Reinhold * @since 1.1 diff --git a/src/java.base/share/classes/java/io/Writer.java b/src/java.base/share/classes/java/io/Writer.java index e023458c09931..4af64c639312e 100644 --- a/src/java.base/share/classes/java/io/Writer.java +++ b/src/java.base/share/classes/java/io/Writer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -146,31 +146,25 @@ public void close() throws IOException { } /** - * Returns a {@code Writer} that writes characters into an - * {@code Appendable}. The writer is initially open and writing appends - * after the last character in the appendable. - * - *

If the appendable is a {code Writer}, it is returned. + * Returns a {@code Writer} that writes characters into a + * {@code StringBuilder}. The writer is initially open and writing appends + * after the last character in the string builder. * *

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

If the appendable changes while the writer is open, e.g. the length + *

If the string builder changes while the writer is open, e.g. the length * changes, the behavior is undefined. * - * @param a {@code Appendable} consuming the character stream. - * @return a {@code Writer} which writes characters into {@code a}, or - * {@code a} if it is a {@code Writer}. - * @throws NullPointerException if {@code a} is {@code null} + * @param sb {@code StringBuilder} consuming the character stream. + * @return a {@code Writer} which writes characters into {@code sb}. + * @throws NullPointerException if {@code sb} is {@code null} * * @since 25 */ - public static Writer of(final Appendable a) { - Objects.requireNonNull(a); - - if (a instanceof Writer w) - return w; + public static Writer of(final StringBuilder sb) { + Objects.requireNonNull(sb); return new Writer() { private boolean isClosed; @@ -184,7 +178,7 @@ private void ensureOpen() throws IOException { @Override public void write(int c) throws IOException { ensureOpen(); - a.append((char) c); + sb.append((char) c); } @Override @@ -194,73 +188,49 @@ public void write(char[] cbuf, int off, int len) throws IOException { if (len == 0) { return; } - switch (a) { - case StringBuilder sb -> sb.append(cbuf, off, len); - case StringBuffer sb -> sb.append(cbuf, off, len); - case CharBuffer cb -> cb.put(cbuf, off, len); - default -> { - for (int i = 0; i < len; i++) - a.append(cbuf[off + i]); - } - } + sb.append(cbuf, off, len); } @Override public void write(String str) throws IOException { ensureOpen(); - switch (a) { - case StringBuilder sb -> sb.append(str); - case StringBuffer sb -> sb.append(str); - case CharBuffer cb -> cb.put(str); - default -> a.append(str); - } + sb.append(str); } @Override public void write(String str, int off, int len) throws IOException { ensureOpen(); - a.append(str, off, off + len); + sb.append(str, off, off + len); } @Override public Writer append(CharSequence csq) throws IOException { ensureOpen(); - a.append(csq); + sb.append(csq); return this; } @Override public Writer append(CharSequence csq, int start, int end) throws IOException { ensureOpen(); - a.append(csq, start, end); + sb.append(csq, start, end); return this; } @Override public Writer append(char c) throws IOException { ensureOpen(); - a.append(c); + sb.append(c); return this; } @Override public void flush() throws IOException { ensureOpen(); - implFlush(); - } - - private void implFlush() throws IOException { - if (a instanceof Flushable f) - f.flush(); } @Override public void close() throws IOException { - if (isClosed) - return; - implFlush(); - if (a instanceof Closable c) - c.close(); isClosed = true; } }; diff --git a/src/java.base/share/classes/java/nio/X-Buffer.java.template b/src/java.base/share/classes/java/nio/X-Buffer.java.template index d089155abf9d6..265df9b399d61 100644 --- a/src/java.base/share/classes/java/nio/X-Buffer.java.template +++ b/src/java.base/share/classes/java/nio/X-Buffer.java.template @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 diff --git a/test/jdk/java/io/Writer/Of.java b/test/jdk/java/io/Writer/Of.java index 685161af94b0f..8c8255e6d0825 100644 --- a/test/jdk/java/io/Writer/Of.java +++ b/test/jdk/java/io/Writer/Of.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 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 @@ -24,10 +24,6 @@ import java.io.Writer; import java.io.StringWriter; import java.io.IOException; -import java.io.StringWriter; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.ReadOnlyBufferException; import java.util.function.Supplier; import org.testng.annotations.*; @@ -36,7 +32,7 @@ /* * @test - * @bug 1234567 + * @bug 8353795 * @summary Check for expected behavior of Writer.of(). * @run testng Of */ @@ -56,10 +52,7 @@ public String toString() { @DataProvider public static Config[] writers() { var sw = new StringWriter(); - var sbuf = new StringBuffer(); var sbld = new StringBuilder(); - var dcb = ByteBuffer.allocateDirect(CONTENT.length() * 2).asCharBuffer(); - var wcb = CharBuffer.wrap(new char[CONTENT.length()]); var w = new Writer() { private String s = ""; private boolean isClosed; @@ -112,43 +105,10 @@ public String toString() { return s; } }; - var a = new Appendable() { - private String s = ""; - - @Override - public Appendable append(char c) throws IOException { - s += c; - return this; - } - - @Override - public Appendable append(CharSequence csq) throws IOException { - s += String.valueOf(csq); - return this; - } - - @Override - public Appendable append(CharSequence csq, int start, int end) - throws IOException { - s += String.valueOf(csq).subSequence(start, end); - return this; - } - - @Override - public String toString() { - return s; - } - }; return new Config[] { new Config("StringWriter", sw, sw::toString), - new Config("StringBuffer", Writer.of(sbuf), sbuf::toString), new Config("StringBuilder", Writer.of(sbld), sbld::toString), - new Config("Direct CharBuffer", Writer.of(dcb), - () -> { dcb.flip(); return dcb.toString(); }), - new Config("Wrapped CharBuffer", Writer.of(wcb), - () -> { wcb.flip(); return wcb.toString(); }), new Config("Custom Writer", w, w::toString), - new Config("Custom Appendable", Writer.of(a), a::toString) }; } From 75efb7fda49b058bee9d5f23bb219b6f16692a44 Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Sat, 5 Apr 2025 17:31:16 +0000 Subject: [PATCH 08/10] Removed unused import --- src/java.base/share/classes/java/io/Writer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/java.base/share/classes/java/io/Writer.java b/src/java.base/share/classes/java/io/Writer.java index 4af64c639312e..54aaea60ab5d8 100644 --- a/src/java.base/share/classes/java/io/Writer.java +++ b/src/java.base/share/classes/java/io/Writer.java @@ -25,7 +25,6 @@ package java.io; -import java.nio.CharBuffer; import java.util.Objects; /** From f48b99995723b6c4dd73014015f50b1047ff28cd Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Sun, 4 May 2025 18:14:20 +0200 Subject: [PATCH 09/10] Update Of.java Applied changnes proposed by @liach: "the default toString already includes id=..., so I usually don't provide an explicit override to make the code concise." --- test/jdk/java/io/Writer/Of.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/jdk/java/io/Writer/Of.java b/test/jdk/java/io/Writer/Of.java index 8c8255e6d0825..03eb28f61b029 100644 --- a/test/jdk/java/io/Writer/Of.java +++ b/test/jdk/java/io/Writer/Of.java @@ -39,12 +39,7 @@ public class Of { private static final String CONTENT = "Some Writer Test"; - private static record Config(String id, Writer writer, Supplier spy) { - @Override - public String toString() { - return id; // allows to identify config when test case fails - } - }; + private static record Config(String id, Writer writer, Supplier spy) {}; /* * Writers to be tested. From ea435ed1090d8f028262f5900baf0a00472726d9 Mon Sep 17 00:00:00 2001 From: Markus KARG Date: Sun, 4 May 2025 18:17:04 +0200 Subject: [PATCH 10/10] Undone copyright update of otherwise unchanged file. --- src/java.base/share/classes/java/nio/X-Buffer.java.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/java.base/share/classes/java/nio/X-Buffer.java.template b/src/java.base/share/classes/java/nio/X-Buffer.java.template index 265df9b399d61..d089155abf9d6 100644 --- a/src/java.base/share/classes/java/nio/X-Buffer.java.template +++ b/src/java.base/share/classes/java/nio/X-Buffer.java.template @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 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