Skip to content

Commit b4e78a0

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 - Fix history handling to properly attach to LineReader - Fix completion candidates to use full names instead of suffixes - Remove unused imports and fields - Use AggregateCompleter to support multiple completers - Fix readLine method signature for JLine 3 API - Sync jansi version in grails-forge to 2.4.2
1 parent f9b2f71 commit b4e78a0

36 files changed

Lines changed: 394 additions & 367 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: 98 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -18,39 +18,33 @@
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;
29-
import java.util.Collection;
3027
import java.util.List;
3128
import java.util.Stack;
3229

3330
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
3431
import org.codehaus.groovy.runtime.StackTraceUtils;
3532
import org.codehaus.groovy.runtime.typehandling.NumberMath;
3633

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;
4734
import org.apache.tools.ant.BuildException;
4835
import org.fusesource.jansi.Ansi;
4936
import org.fusesource.jansi.Ansi.Color;
5037
import org.fusesource.jansi.AnsiConsole;
38+
import org.jline.reader.Completer;
39+
import org.jline.reader.History;
40+
import org.jline.reader.LineReader;
41+
import org.jline.reader.LineReaderBuilder;
42+
import org.jline.reader.impl.completer.AggregateCompleter;
43+
import org.jline.reader.impl.history.DefaultHistory;
44+
import org.jline.terminal.Terminal;
45+
import org.jline.terminal.TerminalBuilder;
5146

5247
import grails.util.Environment;
53-
import org.grails.build.interactive.CandidateListCompletionHandler;
5448
import org.grails.build.logging.GrailsConsoleErrorPrintStream;
5549
import org.grails.build.logging.GrailsConsolePrintStream;
5650

@@ -114,7 +108,7 @@ public class GrailsConsole implements ConsoleLogger {
114108
/**
115109
* The reader to read info from the console
116110
*/
117-
ConsoleReader reader;
111+
LineReader reader;
118112

119113
Terminal terminal;
120114

@@ -123,6 +117,11 @@ public class GrailsConsole implements ConsoleLogger {
123117

124118
History history;
125119

120+
/**
121+
* List of completers to be aggregated for tab completion
122+
*/
123+
private final List<Completer> completers = new java.util.ArrayList<>();
124+
126125
/**
127126
* The category of the current output
128127
*/
@@ -179,8 +178,8 @@ protected GrailsConsole() throws IOException {
179178
* @throws IOException
180179
*/
181180
public void reinitialize(InputStream systemIn, PrintStream systemOut, PrintStream systemErr) throws IOException {
182-
if (reader != null) {
183-
reader.shutdown();
181+
if (terminal != null) {
182+
terminal.close();
184183
}
185184
initialize(systemIn, systemOut, systemErr);
186185
}
@@ -190,20 +189,10 @@ protected void initialize(InputStream systemIn, PrintStream systemOut, PrintStre
190189

191190
redirectSystemOutAndErr(true);
192191

193-
System.setProperty(ShutdownHooks.JLINE_SHUTDOWNHOOK, "false");
194-
195192
if (isInteractiveEnabled()) {
196-
reader = createConsoleReader(systemIn);
197-
reader.setBellEnabled(false);
198-
reader.setCompletionHandler(new CandidateListCompletionHandler());
199-
if (isActivateTerminal()) {
200-
terminal = createTerminal();
201-
}
202-
193+
terminal = createTerminal();
203194
history = prepareHistory();
204-
if (history != null) {
205-
reader.setHistory(history);
206-
}
195+
reader = createLineReader(terminal, history);
207196
} else if (isActivateTerminal()) {
208197
terminal = createTerminal();
209198
}
@@ -251,51 +240,67 @@ private boolean readPropOrTrue(String prop) {
251240
return property == null ? true : Boolean.valueOf(property);
252241
}
253242

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);
243+
protected LineReader createLineReader(Terminal terminal, History history) throws IOException {
244+
LineReaderBuilder builder = LineReaderBuilder.builder()
245+
.terminal(terminal)
246+
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true);
247+
if (history != null) {
248+
builder.variable(LineReader.HISTORY_FILE, new File(System.getProperty("user.home"), HISTORYFILE).toPath());
249+
builder.history(history);
265250
}
251+
return builder.build();
266252
}
267253

268254
/**
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.
255+
* Creates the instance of Terminal used directly in GrailsConsole.
272256
*/
273-
protected Terminal createTerminal() {
274-
terminal = TerminalFactory.create();
275-
if (isWindows()) {
276-
terminal.setEchoEnabled(true);
277-
}
257+
protected Terminal createTerminal() throws IOException {
258+
Terminal terminal = TerminalBuilder.builder()
259+
.system(true)
260+
.build();
278261
return terminal;
279262
}
280263

281264
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-
}
265+
completers.clear();
266+
rebuildLineReader();
267+
}
268+
269+
public void addCompleter(Completer completer) {
270+
if (completer != null) {
271+
completers.add(completer);
272+
rebuildLineReader();
273+
}
274+
}
275+
276+
/**
277+
* Rebuilds the LineReader with all registered completers using an AggregateCompleter.
278+
* This is necessary because JLine 3 LineReader is immutable once created.
279+
*/
280+
private void rebuildLineReader() {
281+
if (terminal != null) {
282+
try {
283+
LineReaderBuilder builder = LineReaderBuilder.builder()
284+
.terminal(terminal)
285+
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true);
286+
287+
if (!completers.isEmpty()) {
288+
builder.completer(new AggregateCompleter(completers));
289+
}
288290

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);
291+
if (history != null) {
292+
builder.variable(LineReader.HISTORY_FILE, new File(System.getProperty("user.home"), HISTORYFILE).toPath());
293+
builder.history(history);
294+
}
295+
reader = builder.build();
296+
} catch (Exception e) {
297+
// ignore
293298
}
294299
}
295300
}
296301

297302
/**
298-
* Prepares a history file to be used by the ConsoleReader. This file
303+
* Prepares a history file to be used by the LineReader. This file
299304
* will live in the home directory of the user.
300305
*/
301306
protected History prepareHistory() throws IOException {
@@ -307,7 +312,7 @@ protected History prepareHistory() throws IOException {
307312
// can't create the file, so no history for you
308313
}
309314
}
310-
return file.canWrite() ? new FileHistory(file) : null;
315+
return file.canWrite() ? new DefaultHistory() : null;
311316
}
312317

313318
public boolean isWindows() {
@@ -334,8 +339,12 @@ public static synchronized void removeInstance() {
334339
if (instance != null) {
335340
instance.removeShutdownHook();
336341
instance.restoreOriginalSystemOutAndErr();
337-
if (instance.getReader() != null) {
338-
instance.getReader().shutdown();
342+
if (instance.terminal != null) {
343+
try {
344+
instance.terminal.close();
345+
} catch (IOException e) {
346+
// ignore
347+
}
339348
}
340349
instance = null;
341350
}
@@ -348,24 +357,18 @@ public void beforeShutdown() {
348357

349358
protected void restoreTerminal() {
350359
try {
351-
terminal.restore();
360+
if (terminal != null) {
361+
terminal.close();
362+
}
352363
} catch (Exception e) {
353364
// ignore
354365
}
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-
}
363366
}
364367

365368
protected void persistHistory() {
366-
if (history instanceof Flushable) {
369+
if (history != null && reader != null) {
367370
try {
368-
((Flushable) history).flush();
371+
history.save();
369372
} catch (Throwable e) {
370373
// ignore exception
371374
}
@@ -442,7 +445,7 @@ public boolean isStacktrace() {
442445
*/
443446
public InputStream getInput() {
444447
assertAllowInput();
445-
return reader.getInput();
448+
return terminal != null ? terminal.input() : System.in;
446449
}
447450

448451
private void assertAllowInput() {
@@ -471,7 +474,7 @@ public void setLastMessage(String lastMessage) {
471474
this.lastMessage = lastMessage;
472475
}
473476

474-
public ConsoleReader getReader() {
477+
public LineReader getReader() {
475478
return reader;
476479
}
477480

@@ -674,7 +677,7 @@ private void logSimpleError(String msg) {
674677
}
675678

676679
public boolean isAnsiEnabled() {
677-
return Ansi.isEnabled() && (terminal != null && terminal.isAnsiSupported()) && ansiEnabled;
680+
return Ansi.isEnabled() && (terminal != null && !"dumb".equals(terminal.getType())) && ansiEnabled;
678681
}
679682

680683
/**
@@ -875,10 +878,17 @@ private String readLine(String prompt, boolean secure) {
875878
assertAllowInput(prompt);
876879
userInputActive = true;
877880
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());
881+
if (secure) {
882+
return reader.readLine(prompt, SECURE_MASK_CHAR);
883+
} else if (defaultInputMask == null) {
884+
return reader.readLine(prompt);
885+
} else {
886+
return reader.readLine(prompt, defaultInputMask);
887+
}
888+
} catch (org.jline.reader.UserInterruptException e) {
889+
return null;
890+
} catch (org.jline.reader.EndOfFileException e) {
891+
return null;
882892
} finally {
883893
userInputActive = false;
884894
}
@@ -1041,4 +1051,12 @@ public Character getDefaultInputMask() {
10411051
public void setDefaultInputMask(Character defaultInputMask) {
10421052
this.defaultInputMask = defaultInputMask;
10431053
}
1054+
1055+
/**
1056+
* Gets the history for the LineReader
1057+
* @return the history
1058+
*/
1059+
public History getHistory() {
1060+
return history;
1061+
}
10441062
}

0 commit comments

Comments
 (0)