Skip to content

Commit 5d68523

Browse files
committed
Upgrade JLine to 3.30.6 and Jansi to 2.4.2
Closes #13752 - Update jline from 2.14.6 to 3.30.6 (org.jline:jline) - Update jansi from 1.18 to 2.4.2 - Migrate all CLI code from JLine 2 API to JLine 3 API: - ConsoleReader -> LineReader/Terminal - jline.console.completer.Completer -> org.jline.reader.Completer - complete(buffer, cursor, candidates) -> complete(reader, line, candidates) - CharSequence candidates -> Candidate objects - Keep JLine 2 (jline:jline:2.14.6) for groovy-groovysh compatibility (Groovy 4.x groovysh requires JLine 2; Groovy 5.x uses JLine 3) - Add TODO comments for JLine 2 removal when upgrading to Groovy 5 - Add JLine 3.30.6 license mapping in SbomPlugin (BSD-3-Clause) - Update RegexCompletorSpec tests for JLine 3 API
1 parent f9b2f71 commit 5d68523

36 files changed

Lines changed: 381 additions & 359 deletions

File tree

build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ class SbomPlugin implements Plugin<Project> {
8888
private static Map<String, String> LICENSE_MAPPING = [
8989
'pkg:maven/org.antlr/antlr4-runtime@4.7.2?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
9090
'pkg:maven/jline/jline@2.14.6?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
91-
'pkg:maven/org.jline/jline@3.23.0?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
91+
'pkg:maven/org.jline/jline@3.23.0?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
92+
'pkg:maven/org.jline/jline@3.30.6?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
9293
'pkg:maven/org.liquibase.ext/liquibase-hibernate5@4.27.0?type=jar': 'Apache-2.0', // maps incorrectly because of https://github.com/liquibase/liquibase/issues/2445 & the base pom does not define a license
9394
'pkg:maven/com.oracle.coherence.ce/coherence-bom@25.03.1?type=pom': 'UPL-1.0', // does not have map based on license id
9495
'pkg:maven/com.oracle.coherence.ce/coherence-bom@22.06.2?type=pom': 'UPL-1.0', // does not have map based on license id

dependencies.gradle

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ ext {
3131
'directory-watcher.version' : '0.19.1',
3232
'gradle-spock.version' : '2.3-groovy-3.0',
3333
'grails-publish-plugin.version' : '0.0.4',
34-
'jansi.version' : '1.18',
34+
'jansi.version' : '2.4.2',
3535
'javaparser-core.version' : '3.27.0',
36-
'jline.version' : '2.14.6',
36+
'jline.version' : '3.30.6',
37+
// TODO: Remove jline2 when upgrading to Groovy 5 (groovy-groovysh 5.x uses JLine 3)
38+
'jline2.version' : '2.14.6',
3739
'jna.version' : '5.17.0',
3840
'jquery.version' : '3.7.1',
3941
'objenesis.version' : '3.4',
@@ -59,7 +61,9 @@ ext {
5961
'grails-publish-plugin' : "org.apache.grails.gradle:grails-publish:${gradleBomDependencyVersions['grails-publish-plugin.version']}",
6062
'jansi' : "org.fusesource.jansi:jansi:${gradleBomDependencyVersions['jansi.version']}",
6163
'javaparser-core' : "com.github.javaparser:javaparser-core:${gradleBomDependencyVersions['javaparser-core.version']}",
62-
'jline' : "jline:jline:${gradleBomDependencyVersions['jline.version']}",
64+
'jline' : "org.jline:jline:${gradleBomDependencyVersions['jline.version']}",
65+
// TODO: Remove jline2 when upgrading to Groovy 5 (groovy-groovysh 5.x uses JLine 3)
66+
'jline2' : "jline:jline:${gradleBomDependencyVersions['jline2.version']}",
6367
'jna' : "net.java.dev.jna:jna:${gradleBomDependencyVersions['jna.version']}",
6468
'objenesis' : "org.objenesis:objenesis:${gradleBomDependencyVersions['objenesis.version']}",
6569
'spring-boot-cli' : "org.springframework.boot:spring-boot-cli:${gradleBomDependencyVersions['spring-boot.version']}",

gradle/docs-dependencies.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ configurations.register('documentation') {
2929
dependencies {
3030
add('documentation', platform(project(':grails-bom')))
3131
add('documentation', 'org.fusesource.jansi:jansi')
32+
// TODO: Remove jline:jline (JLine 2) when upgrading to Groovy 5 (groovy-groovysh 5.x uses JLine 3)
3233
add('documentation', 'jline:jline')
3334
add('documentation', 'com.github.javaparser:javaparser-core')
3435
add('documentation', 'org.apache.groovy:groovy')

grails-bootstrap/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ dependencies {
5050

5151
compileOnly 'io.methvin:directory-watcher'
5252
compileOnly 'org.fusesource.jansi:jansi'
53-
compileOnly 'jline:jline'
53+
compileOnly 'org.jline:jline'
5454
compileOnly 'net.java.dev.jna:jna'
5555

5656
api 'org.yaml:snakeyaml'
5757

5858
testImplementation 'org.apache.groovy:groovy-xml'
5959
testImplementation 'org.apache.groovy:groovy-templates'
6060
testImplementation 'org.fusesource.jansi:jansi'
61-
testImplementation 'jline:jline'
61+
testImplementation 'org.jline:jline'
6262

6363
testImplementation 'org.apache.groovy:groovy-test-junit5'
6464
testImplementation 'org.junit.jupiter:junit-jupiter-api'

grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java

Lines changed: 78 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@
1818
*/
1919
package grails.build.logging;
2020

21-
import java.io.ByteArrayOutputStream;
2221
import java.io.File;
23-
import java.io.Flushable;
2422
import java.io.IOException;
2523
import java.io.InputStream;
2624
import java.io.PrintStream;
2725
import java.io.PrintWriter;
2826
import java.io.StringWriter;
27+
import java.nio.file.Path;
2928
import java.util.Collection;
3029
import java.util.List;
3130
import java.util.Stack;
@@ -34,23 +33,21 @@
3433
import org.codehaus.groovy.runtime.StackTraceUtils;
3534
import org.codehaus.groovy.runtime.typehandling.NumberMath;
3635

37-
import jline.Terminal;
38-
import jline.TerminalFactory;
39-
import jline.UnixTerminal;
40-
import jline.console.ConsoleReader;
41-
import jline.console.completer.Completer;
42-
import jline.console.history.FileHistory;
43-
import jline.console.history.History;
44-
import jline.internal.Log;
45-
import jline.internal.ShutdownHooks;
46-
import jline.internal.TerminalLineSettings;
36+
import org.jline.reader.Completer;
37+
import org.jline.reader.History;
38+
import org.jline.reader.LineReader;
39+
import org.jline.reader.LineReaderBuilder;
40+
import org.jline.reader.impl.history.DefaultHistory;
41+
import org.jline.terminal.Terminal;
42+
import org.jline.terminal.TerminalBuilder;
43+
import org.jline.utils.AttributedString;
44+
import org.jline.utils.AttributedStyle;
4745
import org.apache.tools.ant.BuildException;
4846
import org.fusesource.jansi.Ansi;
4947
import org.fusesource.jansi.Ansi.Color;
5048
import org.fusesource.jansi.AnsiConsole;
5149

5250
import grails.util.Environment;
53-
import org.grails.build.interactive.CandidateListCompletionHandler;
5451
import org.grails.build.logging.GrailsConsoleErrorPrintStream;
5552
import org.grails.build.logging.GrailsConsolePrintStream;
5653

@@ -114,7 +111,7 @@ public class GrailsConsole implements ConsoleLogger {
114111
/**
115112
* The reader to read info from the console
116113
*/
117-
ConsoleReader reader;
114+
LineReader reader;
118115

119116
Terminal terminal;
120117

@@ -179,8 +176,8 @@ protected GrailsConsole() throws IOException {
179176
* @throws IOException
180177
*/
181178
public void reinitialize(InputStream systemIn, PrintStream systemOut, PrintStream systemErr) throws IOException {
182-
if (reader != null) {
183-
reader.shutdown();
179+
if (terminal != null) {
180+
terminal.close();
184181
}
185182
initialize(systemIn, systemOut, systemErr);
186183
}
@@ -190,19 +187,12 @@ protected void initialize(InputStream systemIn, PrintStream systemOut, PrintStre
190187

191188
redirectSystemOutAndErr(true);
192189

193-
System.setProperty(ShutdownHooks.JLINE_SHUTDOWNHOOK, "false");
194-
195190
if (isInteractiveEnabled()) {
196-
reader = createConsoleReader(systemIn);
197-
reader.setBellEnabled(false);
198-
reader.setCompletionHandler(new CandidateListCompletionHandler());
199-
if (isActivateTerminal()) {
200-
terminal = createTerminal();
201-
}
202-
191+
terminal = createTerminal();
192+
reader = createLineReader(terminal);
203193
history = prepareHistory();
204194
if (history != null) {
205-
reader.setHistory(history);
195+
reader.setVariable(LineReader.HISTORY_FILE, new File(System.getProperty("user.home"), HISTORYFILE).toPath());
206196
}
207197
} else if (isActivateTerminal()) {
208198
terminal = createTerminal();
@@ -251,51 +241,50 @@ private boolean readPropOrTrue(String prop) {
251241
return property == null ? true : Boolean.valueOf(property);
252242
}
253243

254-
protected ConsoleReader createConsoleReader(InputStream systemIn) throws IOException {
255-
// need to swap out the output to avoid logging during init
256-
final PrintStream nullOutput = new PrintStream(new ByteArrayOutputStream());
257-
final PrintStream originalOut = Log.getOutput();
258-
try {
259-
Log.setOutput(nullOutput);
260-
ConsoleReader consoleReader = new ConsoleReader(systemIn, out);
261-
consoleReader.setExpandEvents(false);
262-
return consoleReader;
263-
} finally {
264-
Log.setOutput(originalOut);
265-
}
244+
protected LineReader createLineReader(Terminal terminal) throws IOException {
245+
LineReader lineReader = LineReaderBuilder.builder()
246+
.terminal(terminal)
247+
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
248+
.build();
249+
return lineReader;
266250
}
267251

268252
/**
269-
* Creates the instance of Terminal used directly in GrailsConsole. Note that there is also
270-
* another terminal instance created implicitly inside of ConsoleReader. That instance
271-
* is controlled by the jline.terminal system property.
253+
* Creates the instance of Terminal used directly in GrailsConsole.
272254
*/
273-
protected Terminal createTerminal() {
274-
terminal = TerminalFactory.create();
275-
if (isWindows()) {
276-
terminal.setEchoEnabled(true);
277-
}
255+
protected Terminal createTerminal() throws IOException {
256+
Terminal terminal = TerminalBuilder.builder()
257+
.system(true)
258+
.build();
278259
return terminal;
279260
}
280261

281262
public void resetCompleters() {
282-
final ConsoleReader reader = getReader();
283-
if (reader != null) {
284-
Collection<Completer> completers = reader.getCompleters();
285-
for (Completer completer : completers) {
286-
reader.removeCompleter(completer);
287-
}
263+
// In JLine 3, completers are set at LineReader creation time or via setCompleter
264+
// We'll handle this differently - completers are managed via the LineReader
265+
}
288266

289-
// for some unknown reason / bug in JLine you have to iterate over twice to clear the completers (WTF)
290-
completers = reader.getCompleters();
291-
for (Completer completer : completers) {
292-
reader.removeCompleter(completer);
267+
public void addCompleter(Completer completer) {
268+
// In JLine 3, we need to recreate the LineReader with the new completer
269+
// or use an AggregateCompleter. For now, this is a simplified implementation.
270+
if (terminal != null) {
271+
try {
272+
reader = LineReaderBuilder.builder()
273+
.terminal(terminal)
274+
.completer(completer)
275+
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
276+
.build();
277+
if (history != null) {
278+
reader.setVariable(LineReader.HISTORY_FILE, new File(System.getProperty("user.home"), HISTORYFILE).toPath());
279+
}
280+
} catch (Exception e) {
281+
// ignore
293282
}
294283
}
295284
}
296285

297286
/**
298-
* Prepares a history file to be used by the ConsoleReader. This file
287+
* Prepares a history file to be used by the LineReader. This file
299288
* will live in the home directory of the user.
300289
*/
301290
protected History prepareHistory() throws IOException {
@@ -307,7 +296,7 @@ protected History prepareHistory() throws IOException {
307296
// can't create the file, so no history for you
308297
}
309298
}
310-
return file.canWrite() ? new FileHistory(file) : null;
299+
return file.canWrite() ? new DefaultHistory() : null;
311300
}
312301

313302
public boolean isWindows() {
@@ -334,8 +323,12 @@ public static synchronized void removeInstance() {
334323
if (instance != null) {
335324
instance.removeShutdownHook();
336325
instance.restoreOriginalSystemOutAndErr();
337-
if (instance.getReader() != null) {
338-
instance.getReader().shutdown();
326+
if (instance.terminal != null) {
327+
try {
328+
instance.terminal.close();
329+
} catch (IOException e) {
330+
// ignore
331+
}
339332
}
340333
instance = null;
341334
}
@@ -348,24 +341,18 @@ public void beforeShutdown() {
348341

349342
protected void restoreTerminal() {
350343
try {
351-
terminal.restore();
344+
if (terminal != null) {
345+
terminal.close();
346+
}
352347
} catch (Exception e) {
353348
// ignore
354349
}
355-
if (terminal instanceof UnixTerminal) {
356-
// workaround for GRAILS-11494
357-
try {
358-
new TerminalLineSettings().set("sane");
359-
} catch (Exception e) {
360-
// ignore
361-
}
362-
}
363350
}
364351

365352
protected void persistHistory() {
366-
if (history instanceof Flushable) {
353+
if (history != null && reader != null) {
367354
try {
368-
((Flushable) history).flush();
355+
history.save();
369356
} catch (Throwable e) {
370357
// ignore exception
371358
}
@@ -442,7 +429,7 @@ public boolean isStacktrace() {
442429
*/
443430
public InputStream getInput() {
444431
assertAllowInput();
445-
return reader.getInput();
432+
return terminal != null ? terminal.input() : System.in;
446433
}
447434

448435
private void assertAllowInput() {
@@ -471,7 +458,7 @@ public void setLastMessage(String lastMessage) {
471458
this.lastMessage = lastMessage;
472459
}
473460

474-
public ConsoleReader getReader() {
461+
public LineReader getReader() {
475462
return reader;
476463
}
477464

@@ -674,7 +661,7 @@ private void logSimpleError(String msg) {
674661
}
675662

676663
public boolean isAnsiEnabled() {
677-
return Ansi.isEnabled() && (terminal != null && terminal.isAnsiSupported()) && ansiEnabled;
664+
return Ansi.isEnabled() && (terminal != null && terminal.getType() != Terminal.TYPE_DUMB) && ansiEnabled;
678665
}
679666

680667
/**
@@ -875,10 +862,15 @@ private String readLine(String prompt, boolean secure) {
875862
assertAllowInput(prompt);
876863
userInputActive = true;
877864
try {
878-
Character inputMask = secure ? SECURE_MASK_CHAR : defaultInputMask;
879-
return reader.readLine(prompt, inputMask);
880-
} catch (IOException e) {
881-
throw new RuntimeException("Error reading input: " + e.getMessage());
865+
if (secure) {
866+
return reader.readLine(prompt, SECURE_MASK_CHAR);
867+
} else {
868+
return reader.readLine(prompt, defaultInputMask, (String) null);
869+
}
870+
} catch (org.jline.reader.UserInterruptException e) {
871+
return null;
872+
} catch (org.jline.reader.EndOfFileException e) {
873+
return null;
882874
} finally {
883875
userInputActive = false;
884876
}
@@ -1041,4 +1033,12 @@ public Character getDefaultInputMask() {
10411033
public void setDefaultInputMask(Character defaultInputMask) {
10421034
this.defaultInputMask = defaultInputMask;
10431035
}
1036+
1037+
/**
1038+
* Gets the history for the LineReader
1039+
* @return the history
1040+
*/
1041+
public History getHistory() {
1042+
return history;
1043+
}
10441044
}

grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020

2121
import java.io.IOException;
2222

23-
import jline.Terminal;
24-
import jline.UnsupportedTerminal;
23+
import org.jline.terminal.Terminal;
24+
import org.jline.terminal.TerminalBuilder;
2525

2626
/**
2727
* This class is meant to keep changes made in support of Eclipse separate from
@@ -73,8 +73,10 @@ private static Boolean boolProp(String propName) {
7373
}
7474

7575
@Override
76-
protected Terminal createTerminal() {
77-
// unix or windows terminal have no relation at all to the behavior of an Eclipse console.
78-
return new UnsupportedTerminal();
76+
protected Terminal createTerminal() throws IOException {
77+
// For Eclipse, create a dumb terminal that doesn't try to interact with the console
78+
return TerminalBuilder.builder()
79+
.dumb(true)
80+
.build();
7981
}
8082
}

0 commit comments

Comments
 (0)