diff --git a/src/java.base/share/classes/java/io/Console.java b/src/java.base/share/classes/java/io/Console.java index aaa3ba8dd270b..cedb6124a31ee 100644 --- a/src/java.base/share/classes/java/io/Console.java +++ b/src/java.base/share/classes/java/io/Console.java @@ -59,6 +59,13 @@ * on the objects returned by {@link #reader()} and {@link #writer()} may * block in multithreaded scenarios. *

+ * Read and write operations use the {@code Charset}s specified by + * {@link System##stdin.encoding stdin.encoding} and {@link + * System##stdout.encoding stdout.encoding}, respectively. The + * {@code Charset} used for write operations can also be retrieved using + * the {@link #charset()} method. Since {@code Console} is intended for + * interactive use on a terminal, these charsets are typically the same. + *

* Operations that format strings are locale sensitive, using either the * specified {@code Locale}, or the * {@link Locale##default_locale default format Locale} to produce localized @@ -509,17 +516,15 @@ public void flush() { } /** - * Returns the {@link java.nio.charset.Charset Charset} object used for - * the {@code Console}. + * {@return the {@link java.nio.charset.Charset Charset} object used for + * the write operations on this {@code Console}} *

- * The returned charset is used for interpreting the input and output source - * (e.g., keyboard and/or display) specified by the host environment or user, - * which defaults to the one based on {@link System##stdout.encoding stdout.encoding}. - * It may not necessarily be the same as the default charset returned from + * The returned charset is used for encoding the data that is sent to + * the output (e.g., display), specified by the host environment or user. + * It defaults to the one based on {@link System##stdout.encoding stdout.encoding}, + * and may not necessarily be the same as the default charset returned from * {@link java.nio.charset.Charset#defaultCharset() Charset.defaultCharset()}. * - * @return a {@link java.nio.charset.Charset Charset} object used for the - * {@code Console} * @since 17 */ public Charset charset() { @@ -549,7 +554,9 @@ private static UnsupportedOperationException newUnsupportedOperationException() } private static final boolean istty = istty(); - static final Charset CHARSET = + private static final Charset STDIN_CHARSET = + Charset.forName(System.getProperty("stdin.encoding"), UTF_8.INSTANCE); + private static final Charset STDOUT_CHARSET = Charset.forName(System.getProperty("stdout.encoding"), UTF_8.INSTANCE); private static final Console cons = instantiateConsole(); static { @@ -579,7 +586,7 @@ private static Console instantiateConsole() { for (var jcp : ServiceLoader.load(ModuleLayer.boot(), JdkConsoleProvider.class)) { if (consModName.equals(jcp.getClass().getModule().getName())) { - var jc = jcp.console(istty, CHARSET); + var jc = jcp.console(istty, STDIN_CHARSET, STDOUT_CHARSET); if (jc != null) { c = new ProxyingConsole(jc); } @@ -591,7 +598,7 @@ private static Console instantiateConsole() { // If not found, default to built-in Console if (istty && c == null) { - c = new ProxyingConsole(new JdkConsoleImpl(CHARSET)); + c = new ProxyingConsole(new JdkConsoleImpl(STDIN_CHARSET, STDOUT_CHARSET)); } return c; diff --git a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java index 008ed68f56f35..ec94d4ec4d67f 100644 --- a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java +++ b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java @@ -191,10 +191,11 @@ public void flush() { @Override public Charset charset() { - return charset; + return outCharset; } - private final Charset charset; + private final Charset inCharset; + private final Charset outCharset; private final Object readLock; private final Object writeLock; // Must not block while holding this. It is used in the shutdown hook. @@ -364,16 +365,18 @@ public int read(char[] cbuf, int offset, int length) } } - public JdkConsoleImpl(Charset charset) { - Objects.requireNonNull(charset); - this.charset = charset; + public JdkConsoleImpl(Charset inCharset, Charset outCharset) { + Objects.requireNonNull(inCharset); + Objects.requireNonNull(outCharset); + this.inCharset = inCharset; + this.outCharset = outCharset; readLock = new Object(); writeLock = new Object(); restoreEchoLock = new Object(); out = StreamEncoder.forOutputStreamWriter( new FileOutputStream(FileDescriptor.out), writeLock, - charset); + outCharset); pw = new PrintWriter(out, true) { public void close() { } @@ -382,7 +385,7 @@ public void close() { reader = new LineReader(StreamDecoder.forInputStreamReader( new FileInputStream(FileDescriptor.in), readLock, - charset)); + inCharset)); rcb = new char[1024]; } } diff --git a/src/java.base/share/classes/jdk/internal/io/JdkConsoleProvider.java b/src/java.base/share/classes/jdk/internal/io/JdkConsoleProvider.java index 4ac196d67ff9f..f3cc67e85a0e7 100644 --- a/src/java.base/share/classes/jdk/internal/io/JdkConsoleProvider.java +++ b/src/java.base/share/classes/jdk/internal/io/JdkConsoleProvider.java @@ -38,7 +38,8 @@ public interface JdkConsoleProvider { /** * {@return the Console instance, or {@code null} if not available} * @param isTTY indicates if the jvm is attached to a terminal - * @param charset charset of the platform console + * @param inCharset Standard input charset of the platform console + * @param outCharset Standard output charset of the platform console */ - JdkConsole console(boolean isTTY, Charset charset); + JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset); } diff --git a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/JdkConsoleProviderImpl.java b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/JdkConsoleProviderImpl.java index 03a9da22771f9..2c29cb0b6e5de 100644 --- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/JdkConsoleProviderImpl.java +++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/JdkConsoleProviderImpl.java @@ -49,18 +49,18 @@ public class JdkConsoleProviderImpl implements JdkConsoleProvider { * {@inheritDoc} */ @Override - public JdkConsole console(boolean isTTY, Charset charset) { - return new LazyDelegatingJdkConsoleImpl(charset); + public JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset) { + return new LazyDelegatingJdkConsoleImpl(inCharset, outCharset); } private static class LazyDelegatingJdkConsoleImpl implements JdkConsole { - private final Charset charset; + private final Charset outCharset; private volatile boolean jlineInitialized; private volatile JdkConsole delegate; - public LazyDelegatingJdkConsoleImpl(Charset charset) { - this.charset = charset; - this.delegate = new jdk.internal.io.JdkConsoleImpl(charset); + public LazyDelegatingJdkConsoleImpl(Charset inCharset, Charset outCharset) { + this.outCharset = outCharset; + this.delegate = new jdk.internal.io.JdkConsoleImpl(inCharset, outCharset); } @Override @@ -130,7 +130,7 @@ public void flush() { @Override public Charset charset() { - return charset; + return outCharset; } private void flushOldDelegateIfNeeded(JdkConsole oldDelegate) { @@ -157,7 +157,7 @@ private synchronized JdkConsole initializeJLineDelegate() { } try { - Terminal terminal = TerminalBuilder.builder().encoding(charset) + Terminal terminal = TerminalBuilder.builder().encoding(outCharset) .exec(false) .nativeSignals(false) .systemOutput(SystemOutput.SysOut) diff --git a/src/jdk.jshell/share/classes/jdk/jshell/execution/impl/ConsoleImpl.java b/src/jdk.jshell/share/classes/jdk/jshell/execution/impl/ConsoleImpl.java index 0282b62da4cac..5f4defc8afdb6 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/execution/impl/ConsoleImpl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/execution/impl/ConsoleImpl.java @@ -64,7 +64,7 @@ public ConsoleProviderImpl() { private static RemoteConsole console; @Override - public JdkConsole console(boolean isTTY, Charset charset) { + public JdkConsole console(boolean isTTY, Charset inCharset, Charset outCharset) { synchronized (ConsoleProviderImpl.class) { if (remoteOutput != null && remoteInput != null) { return console = new RemoteConsole(remoteOutput, remoteInput); diff --git a/test/jdk/java/io/Console/CharsetTest.java b/test/jdk/java/io/Console/CharsetTest.java index 8bc23fa58f8bb..c8c756def810c 100644 --- a/test/jdk/java/io/Console/CharsetTest.java +++ b/test/jdk/java/io/Console/CharsetTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 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 @@ -27,10 +27,11 @@ import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; +import static jdk.test.lib.Utils.*; /** * @test - * @bug 8264208 8265918 + * @bug 8264208 8265918 8356985 * @summary Tests Console.charset() method. "expect" command in Windows/Cygwin * does not work as expected. Ignoring tests on Windows. * @requires (os.family == "linux") | (os.family == "mac") @@ -50,22 +51,18 @@ public static void main(String... args) throws Throwable { // check "expect" command availability var expect = Paths.get("/usr/bin/expect"); if (!Files.exists(expect) || !Files.isExecutable(expect)) { - System.out.println("'expect' command not found. Test ignored."); - return; + throw new jtreg.SkippedException("'expect' command not found. Test ignored."); } // invoking "expect" command - var testSrc = System.getProperty("test.src", "."); - var testClasses = System.getProperty("test.classes", "."); - var jdkDir = System.getProperty("test.jdk"); OutputAnalyzer output = ProcessTools.executeProcess( "expect", "-n", - testSrc + "/script.exp", - jdkDir + "/bin/java", + TEST_SRC + "/script.exp", + TEST_JDK + "/bin/java", args[0], args[1], - testClasses); + TEST_CLASSES); output.reportDiagnosticSummary(); var eval = output.getExitValue(); if (eval != 0) { diff --git a/test/jdk/java/io/Console/StdinEncodingTest.java b/test/jdk/java/io/Console/StdinEncodingTest.java new file mode 100644 index 0000000000000..1f2ea225f960a --- /dev/null +++ b/test/jdk/java/io/Console/StdinEncodingTest.java @@ -0,0 +1,92 @@ +/* + * 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 + * 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.BufferedReader; +import java.nio.file.Files; +import java.nio.file.Paths; + +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; +import static jdk.test.lib.Utils.*; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @test + * @bug 8356985 + * @summary Tests if "stdin.encoding" is reflected for reading + * the console. "expect" command in Windows/Cygwin does + * not work as expected. Ignoring tests on Windows. + * @requires (os.family == "linux" | os.family == "mac") + * @library /test/lib + * @build csp/* + * @run junit StdinEncodingTest + */ +public class StdinEncodingTest { + + @Test + public void testStdinEncoding() throws Throwable { + // check "expect" command availability + var expect = Paths.get("/usr/bin/expect"); + Assumptions.assumeTrue(Files.exists(expect) && Files.isExecutable(expect), + "'" + expect + "' not found"); + + // invoking "expect" command + OutputAnalyzer output = ProcessTools.executeProcess( + "expect", + "-n", + TEST_SRC + "/stdinEncoding.exp", + TEST_JDK + "/bin/java", + "--module-path", + TEST_CLASSES + "/modules", + "-Dstdin.encoding=Uppercasing", // <- gist of this test + "StdinEncodingTest"); + output.reportDiagnosticSummary(); + var eval = output.getExitValue(); + assertEquals(0, eval, "Test failed. Exit value from 'expect' command: " + eval); + } + + public static void main(String... args) throws Throwable { + // check stdin.encoding + if (!"Uppercasing".equals(System.getProperty("stdin.encoding"))) { + throw new RuntimeException("Uppercasing charset was not set in stdin.encoding"); + } + var con = System.console(); + + // Console.readLine() + System.out.print(con.readLine()); + + // Console.readPassword() + System.out.print(String.valueOf(con.readPassword())); + + // Console.reader() + try (var br = new BufferedReader(con.reader())) { + System.out.print(br.readLine()); + } + + // Wait till the test receives the result + con.readLine(); + } +} diff --git a/test/jdk/java/io/Console/csp/module-info.java b/test/jdk/java/io/Console/csp/module-info.java new file mode 100644 index 0000000000000..9178c3ea631cd --- /dev/null +++ b/test/jdk/java/io/Console/csp/module-info.java @@ -0,0 +1,26 @@ +/* + * 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 + * 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. + */ + +module csp { + provides java.nio.charset.spi.CharsetProvider with provider.UppercasingCharsetProvider; +} diff --git a/test/jdk/java/io/Console/csp/provider/UppercasingCharsetProvider.java b/test/jdk/java/io/Console/csp/provider/UppercasingCharsetProvider.java new file mode 100644 index 0000000000000..ee1142de416a7 --- /dev/null +++ b/test/jdk/java/io/Console/csp/provider/UppercasingCharsetProvider.java @@ -0,0 +1,87 @@ +/* + * 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 + * 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. + */ +package provider; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.spi.CharsetProvider; +import java.util.Collections; +import java.util.Iterator; + +// A test charset provider that decodes every input byte into its uppercase +public class UppercasingCharsetProvider extends CharsetProvider { + + @Override + public Iterator charsets() { + return Collections.singleton(new UppercasingCharsetProvider.UppercasingCharset()).iterator(); + } + + @Override + public Charset charsetForName(String charsetName) { + if (charsetName.equals("Uppercasing")) { + return new UppercasingCharsetProvider.UppercasingCharset(); + } else { + return null; + } + } + + public static class UppercasingCharset extends Charset { + + public UppercasingCharset() { + super("Uppercasing", null); + } + + @Override + public boolean contains(Charset cs) { + return false; + } + + @Override + public CharsetDecoder newDecoder() { + return new UppercasingCharsetDecoder(this, 1, 1); + } + + @Override + public CharsetEncoder newEncoder() { + return null; + } + } + + private static class UppercasingCharsetDecoder extends CharsetDecoder { + public UppercasingCharsetDecoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) { + super(cs, averageCharsPerByte, maxCharsPerByte); + } + + @Override + protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { + while (in.remaining() > 0) { + out.put(Character.toUpperCase((char)in.get())); + } + return CoderResult.UNDERFLOW; + } + } +} diff --git a/test/jdk/java/io/Console/script.exp b/test/jdk/java/io/Console/script.exp index 3c97a3c4143e1..c416990cbe943 100644 --- a/test/jdk/java/io/Console/script.exp +++ b/test/jdk/java/io/Console/script.exp @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2021, 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 @@ -27,6 +27,6 @@ set expected [lrange $argv 2 2] set args [lrange $argv 3 end] regexp {([a-zA-Z_]*).([a-zA-Z0-9\-]*)} $locale dummy lang_region encoding -eval spawn $java -Dsun.stdout.encoding=$encoding -classpath $args CharsetTest +eval spawn $java -Dstdout.encoding=$encoding -classpath $args CharsetTest expect $expected expect eof diff --git a/test/jdk/java/io/Console/stdinEncoding.exp b/test/jdk/java/io/Console/stdinEncoding.exp new file mode 100644 index 0000000000000..23950f195f1bb --- /dev/null +++ b/test/jdk/java/io/Console/stdinEncoding.exp @@ -0,0 +1,63 @@ +# +# 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 +# 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. +# + +set java [lrange $argv 0 end] + +eval spawn $java + +# Console::readLine() +send "abc\r" +expect { + "ABC" { send_error "Console::readLine() received\n" } + timeout { + send_error "Error: Console::readLine() not received\n" + exit 1 + } +} + +# Console::readPassword() +send "def\r" +expect { + "DEF" { send_error "Console::readPassword() received\n" } + timeout { + send_error "Error: Console::readPassword() not received\n" + exit 1 + } +} + +# Console::reader() +send "ghi\r" +expect { + "GHI" { send_error "Console::reader() received\n" } + timeout { + send_error "Error: Console::reader() not received\n" + exit 1 + } +} + +# should receive eof +send "\r" +expect eof + +# success +exit 0