From c33b2bab164c5fc26518ae22efcc5c0a18943b6e Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:57:52 +0100 Subject: [PATCH 01/40] KNOX-3278: Update jline.version to 3.25.1. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a9c4cc46eb..b63b6b612b 100644 --- a/pom.xml +++ b/pom.xml @@ -240,7 +240,7 @@ 3.4 2.47 9.4.57.v20241219 - 3.21.0 + 3.25.1 5.9.0 2.10.8 2.9.0 From 4e642270a33a15b485f5908ae8953216d592d2fd Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:26:06 +0100 Subject: [PATCH 02/40] KNOX-3278: Update groovy to 5.0.4 and jline to 3.30.6, plus jna to 5.18.1. --- gateway-provider-security-jwt/pom.xml | 5 ----- gateway-shell/pom.xml | 10 ---------- pom.xml | 6 +++--- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/gateway-provider-security-jwt/pom.xml b/gateway-provider-security-jwt/pom.xml index 4b729d39b2..9f6a7d2642 100644 --- a/gateway-provider-security-jwt/pom.xml +++ b/gateway-provider-security-jwt/pom.xml @@ -78,11 +78,6 @@ commons-lang3 - - org.jline - jline - - org.apache.knox gateway-test-utils diff --git a/gateway-shell/pom.xml b/gateway-shell/pom.xml index ac24cba33e..0070fd3f2a 100644 --- a/gateway-shell/pom.xml +++ b/gateway-shell/pom.xml @@ -53,12 +53,6 @@ org.apache.groovy groovy-groovysh - - - jline - jline - - org.apache.groovy @@ -72,10 +66,6 @@ org.fusesource.jansi jansi - - org.jline - jline - org.apache.httpcomponents httpcore diff --git a/pom.xml b/pom.xml index b63b6b612b..8922ce8019 100644 --- a/pom.xml +++ b/pom.xml @@ -212,7 +212,7 @@ 4.0.5 2.10.1 1.9.0 - 4.0.29 + 5.0.4 32.1.3-jre 3.4.1 2.2 @@ -240,8 +240,8 @@ 3.4 2.47 9.4.57.v20241219 - 3.25.1 - 5.9.0 + 3.30.6 + 5.18.1 2.10.8 2.9.0 2.5.2 From ce91d2efe452ce2ab29c38c9078363f05c79dd33 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:45:16 +0100 Subject: [PATCH 03/40] KNOX-3278: refactor commands and REPL shell. --- gateway-shell-release/pom.xml | 4 + .../org/apache/knox/gateway/shell/Shell.java | 172 ++++++--- .../commands/AbstractKnoxShellCommand.java | 116 ++++++- .../commands/AbstractSQLCommandSupport.java | 113 +++--- .../gateway/shell/commands/CSVCommand.java | 74 ++-- .../shell/commands/DataSourceCommand.java | 135 ++++--- .../gateway/shell/commands/LoginCommand.java | 96 +++-- .../gateway/shell/commands/SelectCommand.java | 204 +++++------ .../shell/commands/WebHDFSCommand.java | 328 +++++++++--------- 9 files changed, 763 insertions(+), 479 deletions(-) diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml index e8b872b011..31d47caef3 100644 --- a/gateway-shell-release/pom.xml +++ b/gateway-shell-release/pom.xml @@ -43,6 +43,10 @@ false + + META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule + + org.apache.knox.gateway.launcher.Launcher diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 2cd2192fc9..4ff41d2ea7 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -19,7 +19,7 @@ import groovy.ui.GroovyMain; -import org.apache.knox.gateway.shell.commands.AbstractSQLCommandSupport; +import org.apache.knox.gateway.shell.commands.AbstractKnoxShellCommand; import org.apache.knox.gateway.shell.commands.CSVCommand; import org.apache.knox.gateway.shell.commands.DataSourceCommand; import org.apache.knox.gateway.shell.commands.SelectCommand; @@ -31,13 +31,24 @@ import org.apache.knox.gateway.shell.table.KnoxShellTable; import org.apache.knox.gateway.shell.workflow.Workflow; import org.apache.knox.gateway.shell.yarn.Yarn; -import org.apache.groovy.groovysh.AnsiDetector; -import org.apache.groovy.groovysh.Groovysh; -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.AnsiConsole; +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.reader.Completer; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.reader.impl.completer.AggregateCompleter; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; + +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; public class Shell { @@ -56,48 +67,127 @@ public class Shell { KnoxShellTable.class.getName() }; - static { - AnsiConsole.systemInstall(); - Ansi.setDetector( new AnsiDetector() ); - System.setProperty( "groovysh.prompt", "knox" ); - } - - @SuppressWarnings("PMD.DoNotUseThreads") // we need to define a Thread to be able to register a shutdown hook - public static void main( String... args ) throws Exception { - if( args.length > 0 ) { + @SuppressWarnings("PMD.DoNotUseThreads") + public static void main(String... args) throws Exception { + if (args.length > 0) { if (NON_INTERACTIVE_COMMANDS.contains(args[0])) { - final String[] arguments = new String[args.length == 1 ? 1:3]; - arguments[0] = args[0]; - if (args.length > 1) { - arguments[1] = "--gateway"; - arguments[2] = args[1]; - } - KnoxSh.main(arguments); + final String[] arguments = new String[args.length == 1 ? 1 : 3]; + arguments[0] = args[0]; + if (args.length > 1) { + arguments[1] = "--gateway"; + arguments[2] = args[1]; + } + KnoxSh.main(arguments); } else { - GroovyMain.main( args ); + // Execute Groovy scripts headlessly + GroovyMain.main(args); } } else { - Groovysh shell = new Groovysh(); - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { - System.out.println("Closing any open connections ..."); - AbstractSQLCommandSupport sqlcmd = (AbstractSQLCommandSupport) shell.getRegistry().getProperty(":ds"); - sqlcmd.closeConnections(); - sqlcmd = (AbstractSQLCommandSupport) shell.getRegistry().getProperty(":sql"); - sqlcmd.closeConnections(); + // Boot the Interactive JLine 3 REPL + startInteractiveShell(); + } + } + + private static void startInteractiveShell() throws Exception { + // 1. Build Terminal and Engine + Terminal terminal = TerminalBuilder.builder().system(true).name("KnoxShell").build(); + GroovyEngine engine = new GroovyEngine(); + + // 2. Pre-load Knox imports + for (String name : IMPORTS) { + engine.execute("import " + name); + } + + // 3. Instantiate and Map Custom Commands + Map registry = new HashMap<>(); + SelectCommand selectCmd = new SelectCommand(engine, terminal); + DataSourceCommand dsCmd = new DataSourceCommand(engine, terminal); + CSVCommand csvCmd = new CSVCommand(engine, terminal); + WebHDFSCommand hdfsCmd = new WebHDFSCommand(engine, terminal); + + registerCommand(registry, selectCmd); + registerCommand(registry, dsCmd); + registerCommand(registry, csvCmd); + registerCommand(registry, hdfsCmd); + + // 4. Setup Shutdown Hook (Calling closeConnections directly on our object instances) + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\nClosing any open connections..."); + dsCmd.closeConnections(); + selectCmd.closeConnections(); + })); + + // 5. Setup Tab Completers + // StringsCompleter automatically suggests our custom commands (e.g., ":sql", ":fs") + Completer knoxCompleter = new StringsCompleter(registry.keySet()); + Completer groovyCompleter = engine.getScriptCompleter(); + Completer finalCompleter = new AggregateCompleter(knoxCompleter, groovyCompleter); + + // 6. Build the LineReader + LineReader reader = LineReaderBuilder.builder() + .terminal(terminal) + .completer(finalCompleter) + .variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".knoxshell_history")) + .build(); + + terminal.writer().println("Apache Knox Shell"); + terminal.writer().println("Type ':help' for help, ':exit' or ':quit' to quit."); + terminal.writer().flush(); + + // 7. The REPL Loop + while (true) { + try { + String line = reader.readLine("knox> "); + if (line == null) break; + + String trimmed = line.trim(); + if (trimmed.isEmpty()) continue; + + if (trimmed.equalsIgnoreCase(":exit") || trimmed.equalsIgnoreCase(":quit")) { + break; } - }); - for( String name : IMPORTS ) { - shell.execute( "import " + name ); + + // Route custom Knox commands + String[] parts = trimmed.split("\\s+"); + String commandName = parts[0]; + + if (registry.containsKey(commandName)) { + AbstractKnoxShellCommand cmd = registry.get(commandName); + + // Extract arguments to pass to the command + List cmdArgs = new ArrayList<>(); + if (parts.length > 1) { + cmdArgs.addAll(Arrays.asList(parts).subList(1, parts.length)); + } + + Object res = cmd.execute(cmdArgs); + if (res != null) { + terminal.writer().println(res.toString()); + } + } else { + // Fallback to evaluating standard Groovy script logic + Object result = engine.execute(line); + if (result != null) { + terminal.writer().println("==> " + result); + } + } + + terminal.writer().flush(); + + } catch (UserInterruptException | EndOfFileException e) { + // Ctrl+C or Ctrl+D cleanly exits the shell + break; + } catch (Exception e) { + terminal.writer().println("Error: " + e.getMessage()); + terminal.writer().flush(); } - // register custom groovysh commands - shell.register(new SelectCommand(shell)); - shell.register(new DataSourceCommand(shell)); - shell.register(new CSVCommand(shell)); - shell.register(new WebHDFSCommand(shell)); - shell.run( null ); } } -} + private static void registerCommand(Map registry, AbstractKnoxShellCommand cmd) { + registry.put(cmd.getName(), cmd); + if (cmd.getShortcut() != null && !cmd.getShortcut().isEmpty()) { + registry.put(cmd.getShortcut(), cmd); + } + } +} \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java index f2bca42957..099e4ce779 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java @@ -17,47 +17,69 @@ */ package org.apache.knox.gateway.shell.commands; +import java.nio.charset.StandardCharsets; import java.util.List; +import org.apache.groovy.groovysh.jline.GroovyEngine; import org.apache.knox.gateway.shell.CredentialCollectionException; import org.apache.knox.gateway.shell.CredentialCollector; -import org.apache.groovy.groovysh.CommandSupport; -import org.apache.groovy.groovysh.Groovysh; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.terminal.Terminal; -public abstract class AbstractKnoxShellCommand extends CommandSupport { +public abstract class AbstractKnoxShellCommand { static final String KNOXSQLHISTORY = "__knoxsqlhistory"; protected static final String KNOXDATASOURCES = "__knoxdatasources"; + + // NEW FIELDS: Holding the modern execution context + protected final GroovyEngine engine; + protected final Terminal terminal; + private final String name; + private final String shortcut; + private String description; private String usage; private String help; - public AbstractKnoxShellCommand(Groovysh shell, String name, String shortcut) { - super(shell, name, shortcut); + // REFACTORED CONSTRUCTOR: Injects JLine 3 dependencies + public AbstractKnoxShellCommand(GroovyEngine engine, Terminal terminal, String name, String shortcut) { + this.engine = engine; + this.terminal = terminal; + this.name = name; + this.shortcut = shortcut; } - public AbstractKnoxShellCommand(Groovysh shell, String name, String shortcut, - String desc, String usage, String help) { - super(shell, name, shortcut); + // REFACTORED CONSTRUCTOR: Overload with help docs + public AbstractKnoxShellCommand(GroovyEngine engine, Terminal terminal, String name, String shortcut, + String desc, String usage, String help) { + this.engine = engine; + this.terminal = terminal; + this.name = name; + this.shortcut = shortcut; this.description = desc; this.usage = usage; this.help = help; } - @Override + // NEW METHODS: Exposing the command identifiers since CommandSupport is gone + public String getName() { return name; } + public String getShortcut() { return shortcut; } + public String getDescription() { - return description; + return description; } - @Override public String getUsage() { return usage; } - @Override public String getHelp() { return help; } + // NEW ABSTRACT METHOD: Enforces the execution contract for subclasses + public abstract Object execute(List args) throws Exception; + protected String getBindingVariableNameForResultingTable(List args) { String variableName = null; boolean nextOne = false; @@ -74,8 +96,72 @@ protected String getBindingVariableNameForResultingTable(List args) { } protected CredentialCollector login() throws CredentialCollectionException { - KnoxLoginDialog dlg = new KnoxLoginDialog(); - dlg.collect(); - return dlg; + LineReader reader = LineReaderBuilder.builder() + .terminal(terminal) + .build(); + + String collectedUsername; + char[] collectedPassword; + + try { + // 1. Prompt for Username in clear text + collectedUsername = reader.readLine("Username: "); + if (collectedUsername == null || collectedUsername.trim().isEmpty()) { + throw new CredentialCollectionException("Login cancelled: Username cannot be empty."); + } + + // 2. Prompt for Password using the '*' mask character + String passStr = reader.readLine("Password: ", '*'); + collectedPassword = (passStr != null) ? passStr.toCharArray() : new char[0]; + + } catch (org.jline.reader.UserInterruptException e) { + throw new CredentialCollectionException("Login cancelled by user (Ctrl+C)."); + } catch (Exception e) { + throw new CredentialCollectionException("Failed to read credentials from terminal", e); + } + + // 3. Return an anonymous implementation of CredentialCollector + // so we don't break the contract expected by child classes + return new CredentialCollector() { + @Override + public void collect() throws CredentialCollectionException { + // We already collected the credentials in the parent method, + // so this can safely remain a no-op if child classes call it again. + } + + @Override + public String name() { + return collectedUsername; + } + + @Override + public char[] chars() { + return collectedPassword; + } + + @Override + public String string() { + return new String(collectedPassword); + } + + @Override + public byte[] bytes() { + return new String(collectedPassword).getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + @Override + public String type() { + return ""; + } + + @Override + public void setPrompt(String prompt) { + + } + + @Override + public void setName(String name) { + + } + }; } } \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java index 50eaf3abcd..7e407d10ad 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java @@ -28,7 +28,9 @@ import org.apache.knox.gateway.shell.KnoxDataSource; import org.apache.knox.gateway.shell.KnoxSession; import org.apache.knox.gateway.shell.jdbc.JDBCUtils; -import org.apache.groovy.groovysh.Groovysh; + +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.terminal.Terminal; public abstract class AbstractSQLCommandSupport extends AbstractKnoxShellCommand { @@ -36,24 +38,23 @@ public abstract class AbstractSQLCommandSupport extends AbstractKnoxShellCommand protected static final String KNOXDATASOURCE = "__knoxdatasource"; private static final Object KNOXDATASOURCE_CONNECTIONS = "__knoxdatasourceconnections"; - public AbstractSQLCommandSupport(Groovysh shell, String name, String shortcut) { - super(shell, name, shortcut); + public AbstractSQLCommandSupport(GroovyEngine engine, Terminal terminal, String name, String shortcut) { + super(engine, terminal, name, shortcut); } - public AbstractSQLCommandSupport(Groovysh shell, String name, String shortcut, String desc, String usage, - String help) { - super(shell, name, shortcut, desc, usage, help); + public AbstractSQLCommandSupport(GroovyEngine engine, Terminal terminal, String name, String shortcut, String desc, String usage, + String help) { + super(engine, terminal, name, shortcut, desc, usage, help); } @SuppressWarnings("unchecked") protected Connection getConnectionFromSession(KnoxDataSource ds) { - HashMap connections = - (HashMap) getVariables() - .getOrDefault(KNOXDATASOURCE_CONNECTIONS, - new HashMap()); - - Connection conn = connections.get(ds.getName()); - return conn; + //GroovyEngine bindings lack getOrDefault, so we check for null manually + HashMap connections = (HashMap) engine.get((String)KNOXDATASOURCE_CONNECTIONS); + if (connections == null) { + connections = new HashMap<>(); + } + return connections.get(ds.getName()); } @SuppressWarnings("unchecked") @@ -62,45 +63,46 @@ protected Connection getConnection(KnoxDataSource ds, String user, String pass) if (conn == null) { if (user != null && pass != null) { conn = JDBCUtils.createConnection(ds.getConnectStr(), user, pass); - } - else { + } else { conn = JDBCUtils.createConnection(ds.getConnectStr(), null, null); + } + HashMap connections = (HashMap) engine.get((String) KNOXDATASOURCE_CONNECTIONS); + if (connections == null) { + connections = new HashMap<>(); } - HashMap connections = - (HashMap) getVariables() - .getOrDefault(KNOXDATASOURCE_CONNECTIONS, - new HashMap()); connections.put(ds.getName(), conn); - getVariables().put(KNOXDATASOURCE_CONNECTIONS, connections); + engine.put((String) KNOXDATASOURCE_CONNECTIONS, connections); } return conn; } + @SuppressWarnings("unchecked") protected void persistSQLHistory() { - Map> sqlHistories = - (Map>) getVariables().get(KNOXSQLHISTORY); + Map> sqlHistories = (Map>) engine.get(KNOXSQLHISTORY); KnoxSession.persistSQLHistory(sqlHistories); } + @SuppressWarnings("unchecked") protected void persistDataSources() { - Map datasources = - (Map) getVariables().get(KNOXDATASOURCES); + Map datasources = (Map) engine.get(KNOXDATASOURCES); KnoxSession.persistDataSources(datasources); } + @SuppressWarnings("unchecked") protected List getSQLHistory(String dataSourceName) { List sqlHistory = null; - Map> sqlHistories = - (Map>) getVariables().get(KNOXSQLHISTORY); + Map> sqlHistories = (Map>) engine.get(KNOXSQLHISTORY); + if (sqlHistories == null) { // check for persisted histories for known datasources sqlHistories = loadSQLHistories(); if (sqlHistories == null || sqlHistories.isEmpty()) { sqlHistories = new HashMap<>(); - getVariables().put(KNOXSQLHISTORY, sqlHistories); + engine.put(KNOXSQLHISTORY, sqlHistories); } } + // get the history for the specific datasource sqlHistory = sqlHistories.get(dataSourceName); if (sqlHistory == null) { @@ -120,10 +122,13 @@ private Map> loadSQLHistories() { try { sqlHistories = KnoxSession.loadSQLHistories(); if (sqlHistories != null) { - getVariables().put(KNOXSQLHISTORY, sqlHistories); + engine.put(KNOXSQLHISTORY, sqlHistories); } } catch (IOException e) { - e.printStackTrace(); + // Route errors through JLine terminal + terminal.writer().println("Error loading SQL history: " + e.getMessage()); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } return sqlHistories; } @@ -133,10 +138,13 @@ private Map loadDataSources() { try { datasources = KnoxSession.loadDataSources(); if (datasources != null) { - getVariables().put(KNOXDATASOURCES, datasources); + engine.put(KNOXDATASOURCES, datasources); } } catch (IOException e) { - e.printStackTrace(); + //Route errors through JLine terminal + terminal.writer().println("Error loading Data Sources: " + e.getMessage()); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } return datasources; } @@ -167,22 +175,25 @@ protected void addToSQLHistory(List sqlHistory, String sql) { persistSQLHistory(); } + @SuppressWarnings("unchecked") protected void removeFromSQLHistory(String dsName) { - Map> sqlHistories = - (Map>) getVariables().get(KNOXSQLHISTORY); - sqlHistories.remove(dsName); - persistSQLHistory(); + Map> sqlHistories = (Map>) engine.get(KNOXSQLHISTORY); + if (sqlHistories != null) { // Can be null + sqlHistories.remove(dsName); + persistSQLHistory(); + } } + @SuppressWarnings("unchecked") protected Map getDataSources() { - Map datasources = (Map) getVariables().get(KNOXDATASOURCES); + Map datasources = (Map) engine.get(KNOXDATASOURCES); if (datasources == null) { datasources = loadDataSources(); if (datasources != null) { - getVariables().put(KNOXDATASOURCES, datasources); - } - else { + engine.put(KNOXDATASOURCES, datasources); + } else { datasources = new HashMap<>(); + engine.put(KNOXDATASOURCES, datasources); // ADDED: Store the empty map to prevent repeated loading attempts } } return datasources; @@ -191,18 +202,18 @@ protected Map getDataSources() { @SuppressWarnings("unchecked") public void closeConnections() { // close all JDBC connections in the session - called by shutdown hook - HashMap connections = - (HashMap) getVariables() - .getOrDefault(KNOXDATASOURCE_CONNECTIONS, - new HashMap()); - connections.values().forEach(connection->{ - try { - if (!connection.isClosed()) { - connection.close(); + HashMap connections = (HashMap) engine.get((String) KNOXDATASOURCE_CONNECTIONS); + if (connections == null) { + connections = new HashMap<>(); + } + connections.values().forEach(connection -> { + try { + if (!connection.isClosed()) { + connection.close(); + } + } catch (SQLException e) { + // nop } - } catch (SQLException e) { - // nop - } - }); + }); } } \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java index 3c1bf73dfb..b8cbcb78b2 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java @@ -20,63 +20,81 @@ import java.io.IOException; import java.util.List; +import org.apache.groovy.groovysh.jline.GroovyEngine; import org.apache.knox.gateway.shell.table.KnoxShellTable; -import org.apache.groovy.groovysh.Groovysh; + +import org.jline.terminal.Terminal; public class CSVCommand extends AbstractKnoxShellCommand { private static final String USAGE = ":csv [withHeaders] file-url||$variable-name [assign resulting-variable-name]"; private static final String DESC = "Build table from CSV file located at provided URL or KnoxShell $variable-name"; - private boolean withHeaders; - private String url; - public CSVCommand(Groovysh shell) { - super(shell, ":CSV", ":csv", DESC, USAGE, DESC); + // REFACTORED CONSTRUCTOR: Takes engine and terminal instead of shell + public CSVCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, ":CSV", ":csv", DESC, USAGE, DESC); } - @SuppressWarnings("unchecked") @Override public Object execute(List args) { - KnoxShellTable table = null; - String bindVariableName = null; - if (!args.isEmpty()) { - bindVariableName = getBindingVariableNameForResultingTable(args); + // FIXED: Prevent shell crash if user types :csv with no arguments + if (args == null || args.isEmpty()) { + terminal.writer().println("Usage: " + USAGE); + terminal.writer().flush(); + return null; } - if (args.get(0).contentEquals("withHeaders")) { + + KnoxShellTable table = null; + String bindVariableName = getBindingVariableNameForResultingTable(args); + + // FIXED: Moved to local variables to prevent state leaking between executions + boolean withHeaders = false; + String url; + + if ("withHeaders".equalsIgnoreCase(args.get(0))) { withHeaders = true; - url = args.get(1); - } - else { + if (args.size() > 1) { + url = args.get(1); + } else { + terminal.writer().println("Error: Missing file URL or variable name."); + terminal.writer().flush(); + return null; + } + } else { url = args.get(0); } try { if (withHeaders) { if (url.startsWith("$")) { - // a knoxshell variable is a csv file as a string - String csvString = (String) getVariables().get(url.substring(1)); + // REFACTORED: Use engine.get() instead of getVariables().get() + String csvString = (String) engine.get(url.substring(1)); table = KnoxShellTable.builder().csv().withHeaders().string(csvString); - } - else { + } else { table = KnoxShellTable.builder().csv().withHeaders().url(url); } - } - else { + } else { if (url.startsWith("$")) { - // a knoxshell variable is a csv file as a string - String csvString = (String) getVariables().get(url.substring(1)); + // REFACTORED: Use engine.get() instead of getVariables().get() + String csvString = (String) engine.get(url.substring(1)); table = KnoxShellTable.builder().csv().string(csvString); - } - else { + } else { table = KnoxShellTable.builder().csv().url(url); } } } catch (IOException e) { - e.printStackTrace(); + // REFACTORED: Print errors nicely via the JLine 3 terminal + terminal.writer().println("Error parsing CSV: " + e.getMessage()); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } + if (table != null && bindVariableName != null) { - getVariables().put(bindVariableName, table); + // REFACTORED: Use engine.put() instead of getVariables().put() + engine.put(bindVariableName, table); + terminal.writer().println("Assigned resulting table to variable: " + bindVariableName); + terminal.writer().flush(); } + return table; } - -} +} \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index bd01ae9cc8..efa8a61f3f 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -27,61 +27,86 @@ import org.apache.knox.gateway.shell.CredentialCollector; import org.apache.knox.gateway.shell.KnoxDataSource; import org.apache.knox.gateway.shell.table.KnoxShellTable; -import org.apache.groovy.groovysh.Groovysh; + +// NEW IMPORTS: Replacing Groovysh +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; public class DataSourceCommand extends AbstractSQLCommandSupport { - private static final String USAGE = ":ds (add|remove|select) [ds-name, connection-str, driver classname, authntype(none|basic)]"; + private static final String USAGE = ":ds (add|remove|list|select) [ds-name] [connection-str] [driver-classname] [authntype(none|basic)]"; private static final String DESC = "Datasource management commands. Persisted datasources maintain connection details across sessions"; - public DataSourceCommand(Groovysh shell) { - super(shell, ":datasources", ":ds", DESC, USAGE, DESC); + // REFACTORED CONSTRUCTOR + public DataSourceCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, ":datasources", ":ds", DESC, USAGE, DESC); } @SuppressWarnings({"unchecked", "PMD.CloseResource"}) @Override public Object execute(List args) { - Map dataSources = - getDataSources(); - if (args.isEmpty()) { - args.add("list"); - } - if (args.get(0).equalsIgnoreCase("add")) { - KnoxDataSource ds = new KnoxDataSource(args.get(1), - args.get(2), - args.get(3), - args.get(4)); + Map dataSources = getDataSources(); + + // FIXED: Safely default to "list" without mutating the potentially immutable args list + String action = (args == null || args.isEmpty()) ? "list" : args.get(0); + + if (action.equalsIgnoreCase("add")) { + // FIXED: Prevent IndexOutOfBoundsException + if (args.size() < 5) { + terminal.writer().println("Error: Missing arguments for 'add'."); + terminal.writer().println("Usage: :ds add ds-name connection-str driver-classname authntype"); + terminal.writer().flush(); + return null; + } + KnoxDataSource ds = new KnoxDataSource(args.get(1), args.get(2), args.get(3), args.get(4)); dataSources.put(ds.getName(), ds); - getVariables().put(KNOXDATASOURCES, dataSources); + engine.put(KNOXDATASOURCES, dataSources); // REFACTORED: Use engine.put persistDataSources(); } - else if (args.get(0).equalsIgnoreCase("remove")) { + else if (action.equalsIgnoreCase("remove")) { if (dataSources == null || dataSources.isEmpty()) { return "No datasources to remove."; } - // if the removed datasource is currently selected, unselect it - dataSources.remove(args.get(1)); - if (getVariables().get(KNOXDATASOURCE) != null) { - if (args.get(1) != null) { - if (((String)getVariables().get(KNOXDATASOURCE)).equals(args.get(1))) { - System.out.println("unselecting datasource."); - getVariables().put(KNOXDATASOURCE, ""); - } - } - else { - System.out.println("Missing datasource name to remove."); + // FIXED: Prevent IndexOutOfBoundsException + if (args.size() < 2) { + terminal.writer().println("Error: Missing datasource name to remove."); + terminal.writer().flush(); + return null; + } + + String dsName = args.get(1); + dataSources.remove(dsName); + + // REFACTORED: Use engine.get() and engine.put() + if (engine.get(KNOXDATASOURCE) != null) { + if (((String) engine.get(KNOXDATASOURCE)).equals(dsName)) { + terminal.writer().println("Unselecting datasource."); + terminal.writer().flush(); + engine.put(KNOXDATASOURCE, ""); } } - getVariables().put(KNOXDATASOURCES, dataSources); + engine.put(KNOXDATASOURCES, dataSources); persistDataSources(); } - else if (args.get(0).equalsIgnoreCase("list")) { + else if (action.equalsIgnoreCase("list")) { // valid command no additional work needed though } - else if(args.get(0).equalsIgnoreCase("select")) { + else if (action.equalsIgnoreCase("select")) { if (dataSources == null || dataSources.isEmpty()) { return "No datasources to select from."; } + // FIXED: Prevent IndexOutOfBoundsException + if (args.size() < 2) { + terminal.writer().println("Error: Missing datasource name to select."); + terminal.writer().flush(); + return null; + } + KnoxDataSource dsValue = dataSources.get(args.get(1)); + if (dsValue == null) { + return "Error: Datasource '" + args.get(1) + "' not found."; + } + Connection conn = getConnectionFromSession(dsValue); try { if (conn == null || conn.isClosed()) { @@ -92,25 +117,34 @@ else if(args.get(0).equalsIgnoreCase("select")) { try { dlg = login(); } catch (CredentialCollectionException e) { - e.printStackTrace(); - return "Error: Credential collection failure."; + terminal.writer().println("Error: Credential collection failure."); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); + return null; } username = dlg.name(); pass = dlg.chars(); } try { - getConnection(dsValue, username, new String(pass)); + // FIXED: Prevent NullPointerException if pass is null + String passStr = (pass == null) ? null : new String(pass); + getConnection(dsValue, username, passStr); } catch (Exception e) { - e.printStackTrace(); - return "Error: Connection creation failure."; + terminal.writer().println("Error: Connection creation failure."); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); + return null; } } } catch (SQLException e) { - e.printStackTrace(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } + if (dataSources.containsKey(args.get(1))) { - getVariables().put(KNOXDATASOURCE, args.get(1)); + engine.put(KNOXDATASOURCE, args.get(1)); // REFACTORED: Use engine.put } + KnoxShellTable datasource = new KnoxShellTable(); datasource.title("Knox DataSource Selected"); datasource.header("Name").header("Connect String").header("Driver").header("Authn Type"); @@ -118,7 +152,7 @@ else if(args.get(0).equalsIgnoreCase("select")) { return datasource; } else { - return "ERROR: unknown datasources command."; + return "ERROR: unknown datasources command: " + action; } return buildTable(); @@ -128,11 +162,13 @@ private KnoxShellTable buildTable() { KnoxShellTable datasource = new KnoxShellTable(); datasource.title("Knox DataSources"); datasource.header("Name").header("Connect String").header("Driver").header("Authn Type"); + @SuppressWarnings("unchecked") Map dataSources = - (Map) getVariables().get(KNOXDATASOURCES); + (Map) engine.get(KNOXDATASOURCES); // REFACTORED: Use engine.get + if (dataSources != null && !dataSources.isEmpty()) { - for(KnoxDataSource dsValue : dataSources.values()) { + for (KnoxDataSource dsValue : dataSources.values()) { datasource.row().value(dsValue.getName()).value(dsValue.getConnectStr()).value(dsValue.getDriver()).value(dsValue.getAuthnType()); } } @@ -140,8 +176,19 @@ private KnoxShellTable buildTable() { } public static void main(String[] args) { - DataSourceCommand cmd = new DataSourceCommand(new Groovysh()); - List args2 = new ArrayList<>(); - cmd.execute(args2); + try { + Terminal terminal = TerminalBuilder.builder().system(true).build(); + GroovyEngine engine = new GroovyEngine(); + DataSourceCommand cmd = new DataSourceCommand(engine, terminal); + + List args2 = new ArrayList<>(); + Object res = cmd.execute(args2); + if (res != null) { + terminal.writer().println(res); + terminal.writer().flush(); + } + } catch (Exception e) { + e.printStackTrace(); + } } -} +} \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java index e82e72d68c..77e5c9da96 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java @@ -21,38 +21,94 @@ import java.util.ArrayList; import java.util.List; -import org.apache.knox.gateway.shell.CredentialCollectionException; import org.apache.knox.gateway.shell.KnoxSession; -import org.apache.groovy.groovysh.CommandSupport; -import org.apache.groovy.groovysh.Groovysh; -public class LoginCommand extends CommandSupport { +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; - public LoginCommand(Groovysh shell) { - super(shell, ":login", ":lgn"); +public class LoginCommand extends AbstractKnoxShellCommand { + + // REFACTORED CONSTRUCTOR + public LoginCommand(GroovyEngine engine, Terminal terminal) { + // Pass identifiers and docs up to AbstractKnoxShellCommand + super(engine, terminal, ":login", ":lgn", + "Establishes a Knox session", + "Usage: :login ", + "Establishes a Knox session using terminal credentials"); } - @SuppressWarnings("unchecked") @Override public Object execute(List args) { + // FIXED: Prevent IndexOutOfBounds if user types :login without a URL + if (args == null || args.isEmpty()) { + terminal.writer().println("Error: Knox Gateway URL required."); + terminal.writer().println(getUsage()); + terminal.writer().flush(); + return null; + } + + String url = args.get(0); KnoxSession session = null; - KnoxLoginDialog dlg = new KnoxLoginDialog(); + try { - dlg.collect(); - if (dlg.ok) { - session = KnoxSession.login(args.get(0), dlg.username, new String(dlg.pass)); - getVariables().put("__knoxsession", session); + // REPLACED KnoxLoginDialog with JLine 3 native prompting + LineReader reader = LineReaderBuilder.builder() + .terminal(terminal) + .build(); + + // 1. Prompt for Username (Clear text) + String username = reader.readLine("Username: "); + if (username == null || username.trim().isEmpty()) { + terminal.writer().println("Login cancelled: Username cannot be empty."); + terminal.writer().flush(); + return null; } - } catch (CredentialCollectionException | URISyntaxException e) { - e.printStackTrace(); + + // 2. Prompt for Password (Masked with '*') + // JLine 3 intercepts keystrokes and prints the mask char instead of the actual key + String password = reader.readLine("Password: ", '*'); + + if (password != null) { + // Create the session + session = KnoxSession.login(url, username, password); + + // Inject the session into the Groovy 5 environment + engine.put("__knoxsession", session); + + terminal.writer().println("Session established for: " + url); + terminal.writer().flush(); + } else { + terminal.writer().println("Login cancelled."); + terminal.writer().flush(); + } + + } catch (URISyntaxException e) { + terminal.writer().println("Invalid URL syntax: " + e.getMessage()); + terminal.writer().flush(); + } catch (Exception e) { + terminal.writer().println("Failed to establish session: " + e.getMessage()); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } - return "Session established for: " + args.get(0); + + return session; // Returning the session object } public static void main(String[] args) { - LoginCommand cmd = new LoginCommand(new Groovysh()); - List args2 = new ArrayList<>(); - args2.add("https://localhost:8443/gateway"); - cmd.execute(args2); + try { + // Test using JLine 3 Terminal + Terminal terminal = TerminalBuilder.builder().system(true).build(); + GroovyEngine engine = new GroovyEngine(); + LoginCommand cmd = new LoginCommand(engine, terminal); + + List args2 = new ArrayList<>(); + args2.add("https://localhost:8443/gateway/sandbox"); + cmd.execute(args2); + } catch (Exception e) { + e.printStackTrace(); + } } -} +} \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java index 6f8ca169d8..cbe32e089d 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java @@ -7,7 +7,7 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -17,8 +17,6 @@ */ package org.apache.knox.gateway.shell.commands; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -26,160 +24,130 @@ import java.util.List; import java.util.Map; -import javax.swing.Box; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; - import org.apache.knox.gateway.shell.CredentialCollector; import org.apache.knox.gateway.shell.KnoxDataSource; import org.apache.knox.gateway.shell.table.KnoxShellTable; -import org.apache.groovy.groovysh.Groovysh; -public class SelectCommand extends AbstractSQLCommandSupport implements KeyListener { +// Replacing Swing/AWT and Groovysh +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.terminal.Terminal; + +// REMOVED: "implements KeyListener" - JLine 3 handles this natively! +public class SelectCommand extends AbstractSQLCommandSupport { private static final String USAGE = ":sql [assign resulting-variable-name]"; private static final String DESC = "Build table from SQL ResultSet"; private static final String KNOXDATASOURCE = "__knoxdatasource"; - private JTextArea sqlField; - private List sqlHistory; - private int historyIndex = -1; - - public SelectCommand(Groovysh shell) { - super(shell, ":SQL", ":sql", DESC, USAGE, DESC); - } - - @Override - public void keyPressed(KeyEvent event) { - int code = event.getKeyCode(); - boolean setFromHistory = false; - if (sqlHistory != null && !sqlHistory.isEmpty()) { - if (historyIndex == -1) { - historyIndex = sqlHistory.size() + 1; - } - if (code == KeyEvent.VK_KP_UP || - code == KeyEvent.VK_UP) { - if (historyIndex > 0) { - historyIndex -= 1; - } - setFromHistory = true; - } - else if (code == KeyEvent.VK_KP_DOWN || - code == KeyEvent.VK_DOWN) { - if (historyIndex < sqlHistory.size() - 1) { - historyIndex += 1; - setFromHistory = true; - } - } - if (setFromHistory) { - sqlField.setText(sqlHistory.get(historyIndex)); - sqlField.invalidate(); - } - } - } - - @Override - public void keyReleased(KeyEvent event) { - } - @Override - public void keyTyped(KeyEvent event) { + public SelectCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, ":SQL", ":sql", DESC, USAGE, DESC); } @SuppressWarnings({"unchecked", "PMD.CloseResource"}) @Override public Object execute(List args) { - boolean ok = false; - String sql = ""; String bindVariableName = null; KnoxShellTable table = null; - if (!args.isEmpty()) { + if (args != null && !args.isEmpty()) { bindVariableName = getBindingVariableNameForResultingTable(args); } - String dsName = (String) getVariables().get(KNOXDATASOURCE); + String dsName = (String) engine.get(KNOXDATASOURCE); Map dataSources = getDataSources(); KnoxDataSource ds = null; + if (dsName == null || dsName.isEmpty()) { if (dataSources == null || dataSources.isEmpty()) { - return "please configure a datasource with ':datasources add {name} {connectStr} {driver} {authntype: none|basic}'."; - } - else if (dataSources.size() == 1) { + return "Please configure a datasource with ':datasources add {name} {connectStr} {driver} {authntype: none|basic}'."; + } else if (dataSources.size() == 1) { dsName = (String) dataSources.keySet().toArray()[0]; - } - else { - return "mulitple datasources configured. please disambiguate with ':datasources select {name}'."; + } else { + return "Multiple datasources configured. Please disambiguate with ':datasources select {name}'."; } } - sqlHistory = getSQLHistory(dsName); - historyIndex = (sqlHistory != null && !sqlHistory.isEmpty()) ? sqlHistory.size() - 1 : -1; - ds = dataSources.get(dsName); if (ds != null) { - JLabel jl = new JLabel("Query: "); - sqlField = new JTextArea(5,40); - sqlField.addKeyListener(this); - sqlField.setLineWrap(true); - JScrollPane scrollPane = new JScrollPane(sqlField); - Box box = Box.createHorizontalBox(); - box.add(jl); - box.add(scrollPane); - - // JDK-5018574 : Unable to set focus to another component in JOptionPane - SwingUtils.workAroundFocusIssue(sqlField); - - int x = JOptionPane.showConfirmDialog(null, box, - "SQL Query Input", JOptionPane.OK_CANCEL_OPTION); - - if (x == JOptionPane.OK_OPTION) { - ok = true; - sql = sqlField.getText(); - addToSQLHistory(dsName, sql); - historyIndex = -1; + String sql = promptForSQL(dsName); + + if (sql == null || sql.trim().isEmpty()) { + return "Query cancelled or empty."; } - //KnoxShellTable.builder().jdbc().connect("jdbc:derby:codejava/webdb1").driver("org.apache.derby.jdbc.EmbeddedDriver").username("lmccay").pwd("xxxx").sql("SELECT * FROM book"); + addToSQLHistory(dsName, sql); + try { - if (ok) { - System.out.println(sql); - try { - Connection conn = getConnectionFromSession(ds); - if (conn == null || conn.isClosed()) { - String username = null; - char[] pass = null; - if (ds.getAuthnType().equalsIgnoreCase("basic")) { - CredentialCollector dlg = login(); - username = dlg.name(); - pass = dlg.chars(); - } - conn = getConnection(ds, username, new String(pass)); - } - try (Statement statement = conn.createStatement()) { - if (statement.execute(sql)) { - try (ResultSet resultSet = statement.getResultSet()) { - table = KnoxShellTable.builder().jdbc().resultSet(resultSet); - } - } - } + terminal.writer().println("Executing: " + sql); + terminal.writer().flush(); + + Connection conn = getConnectionFromSession(ds); + if (conn == null || conn.isClosed()) { + String username = null; + char[] pass = null; + if ("basic".equalsIgnoreCase(ds.getAuthnType())) { + CredentialCollector dlg = login(); + username = dlg.name(); + pass = dlg.chars(); } - catch (SQLException e) { - System.out.println("SQL Exception encountered... " + e.getMessage()); + // NullPointerException prevention for pass + String passStr = (pass == null) ? null : new String(pass); + conn = getConnection(ds, username, passStr); + } + + try (Statement statement = conn.createStatement()) { + if (statement.execute(sql)) { + try (ResultSet resultSet = statement.getResultSet()) { + table = KnoxShellTable.builder().jdbc().resultSet(resultSet); + } } } + } catch (SQLException e) { + terminal.writer().println("SQL Exception encountered: " + e.getMessage()); + terminal.writer().flush(); + } catch (Exception e) { + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } - catch (Exception e) { - e.printStackTrace(); - } - } - else { - return "please select a datasource via ':datasources select {name}'."; + } else { + return "Please select a datasource via ':datasources select {name}'."; } + if (table != null && bindVariableName != null) { - getVariables().put(bindVariableName, table); + engine.put(bindVariableName, table); // REFACTORED: Use engine.put() + terminal.writer().println("Assigned resulting table to variable: " + bindVariableName); + terminal.writer().flush(); } + return table; } -} + + /** + * Replaces the old Swing JOptionPane and KeyListener with a native JLine 3 prompt. + */ + private String promptForSQL(String dsName) { + try { + // Build a temporary LineReader just for the SQL prompt + LineReader sqlReader = LineReaderBuilder.builder() + .terminal(terminal) + .build(); + + // Load the specific SQL history for this datasource into JLine + List sqlHistory = getSQLHistory(dsName); + if (sqlHistory != null) { + for (String pastQuery : sqlHistory) { + sqlReader.getHistory().add(pastQuery); + } + } + + // Prompt the user in the terminal (Up/Down arrows automatically cycle through the history we just added!) + return sqlReader.readLine("SQL (" + dsName + ")> "); + + } catch (org.jline.reader.UserInterruptException | org.jline.reader.EndOfFileException e) { + // User hit Ctrl+C or Ctrl+D to cancel the prompt + return null; + } + } +} \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java index 4dc6d88881..5f3e4d7f2a 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java @@ -17,7 +17,6 @@ */ package org.apache.knox.gateway.shell.commands; -import java.io.Console; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; @@ -38,127 +37,131 @@ import org.apache.knox.gateway.shell.hdfs.Status.Response; import org.apache.knox.gateway.shell.table.KnoxShellTable; import org.apache.knox.gateway.util.JsonUtils; -import org.apache.groovy.groovysh.Groovysh; + +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; public class WebHDFSCommand extends AbstractKnoxShellCommand { private static final String DESC = "POSIX style commands for Hadoop Filesystems"; private static final String USAGE = "Usage: \n" + - " :fs mounts \n" + - " :fs mount target-topology-url mountpoint-name \n" + - " :fs unmount mountpoint-name \n" + - " :fs ls {target-path} \n" + - " :fs cat {target-path} \n" + - " :fs get {from-path} {to-path} \n" + - " :fs put {from-path} {tp-path} \n" + - " :fs rm {target-path} \n" + - " :fs mkdir {dir-path} \n"; + " :fs mounts \n" + + " :fs mount target-topology-url mountpoint-name \n" + + " :fs unmount mountpoint-name \n" + + " :fs ls {target-path} \n" + + " :fs cat {target-path} \n" + + " :fs get {from-path} {to-path} \n" + + " :fs put {from-path} {to-path} \n" + + " :fs rm {target-path} \n" + + " :fs mkdir {dir-path} \n"; + private Map sessions = new HashMap<>(); - public WebHDFSCommand(Groovysh shell) { - super(shell, ":filesystem", ":fs", DESC, USAGE, DESC); + // REFACTORED CONSTRUCTOR + public WebHDFSCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, ":filesystem", ":fs", DESC, USAGE, DESC); } @Override public Object execute(List args) { Map mounts = getMountPoints(); - if (args.isEmpty()) { - args.add("ls"); + if (mounts == null) { + mounts = new HashMap<>(); } - if (args.get(0).equalsIgnoreCase("mount")) { - String url = args.get(1); - String mountPoint = args.get(2); - return mount(mounts, url, mountPoint); + + String action = (args == null || args.isEmpty()) ? "ls" : args.get(0); + + if (action.equalsIgnoreCase("mount")) { + if (args.size() < 3) return printError("Usage: :fs mount "); + return mount(mounts, args.get(1), args.get(2)); } - else if (args.get(0).equalsIgnoreCase("unmount")) { - String mountPoint = args.get(1); - unmount(mounts, mountPoint); + else if (action.equalsIgnoreCase("unmount")) { + if (args.size() < 2) return printError("Usage: :fs unmount "); + unmount(mounts, args.get(1)); + return "Unmounted " + args.get(1); } - else if (args.get(0).equalsIgnoreCase("mounts")) { + else if (action.equalsIgnoreCase("mounts")) { return listMounts(mounts); } - else if (args.get(0).equalsIgnoreCase("ls")) { - String path = args.get(1); - return listStatus(mounts, path); + else if (action.equalsIgnoreCase("ls")) { + if (args.size() < 2) return printError("Usage: :fs ls "); + return listStatus(mounts, args.get(1)); } - else if (args.get(0).equalsIgnoreCase("put")) { - // Hdfs.put( session ).file( dataFile ).to( dataDir + "/" + dataFile ).now() - // :fs put from-path to-path + else if (action.equalsIgnoreCase("put")) { + if (args.size() < 3) return printError("Usage: :fs put [permissions]"); String localFile = args.get(1); String path = args.get(2); int permission = 755; if (args.size() >= 4) { - permission = Integer.parseInt(args.get(3)); + try { + permission = Integer.parseInt(args.get(3)); + } catch (NumberFormatException e) { + return printError("Invalid permission format. Expected integer."); + } } - return put(mounts, localFile, path, permission); } - else if (args.get(0).equalsIgnoreCase("rm")) { - // Hdfs.rm( session ).file( dataFile ).now() - // :fs rm target-path - String path = args.get(1); - return remove(mounts, path); + else if (action.equalsIgnoreCase("rm")) { + if (args.size() < 2) return printError("Usage: :fs rm "); + return remove(mounts, args.get(1)); } - else if (args.get(0).equalsIgnoreCase("cat")) { - // println Hdfs.get( session ).from( dataDir + "/" + dataFile ).now().string - // :fs cat target-path - String path = args.get(1); - return cat(mounts, path); + else if (action.equalsIgnoreCase("cat")) { + if (args.size() < 2) return printError("Usage: :fs cat "); + return cat(mounts, args.get(1)); } - else if (args.get(0).equalsIgnoreCase("mkdir")) { - // println Hdfs.mkdir( session ).dir( directoryPath ).perm( "777" ).now().string - // :fs mkdir target-path [perms] - String path = args.get(1); - String perms = null; - if (args.size() == 3) { - perms = args.get(2); - } - - return mkdir(mounts, path, perms); + else if (action.equalsIgnoreCase("mkdir")) { + if (args.size() < 2) return printError("Usage: :fs mkdir [perms]"); + String perms = (args.size() == 3) ? args.get(2) : null; + return mkdir(mounts, args.get(1), perms); } - else if (args.get(0).equalsIgnoreCase("get")) { - // println Hdfs.get( session ).from( dataDir + "/" + dataFile ).now().string - // :fs get from-path [to-path] + else if (action.equalsIgnoreCase("get")) { + if (args.size() < 2) return printError("Usage: :fs get [to-path]"); String path = args.get(1); - String mountPoint = determineMountPoint(path); KnoxSession session = getSessionForMountPoint(mounts, mountPoint); + if (session != null) { String from = determineTargetPath(path, mountPoint); - String to = null; - if (args.size() > 2) { - to = args.get(2); - } - else { - to = System.getProperty("user.home") + File.separator + - path.substring(path.lastIndexOf(File.separator)); - } + String to = (args.size() > 2) ? args.get(2) : + System.getProperty("user.home") + File.separator + getFileName(path); return get(mountPoint, from, to); + } else { + return "No session established for mountPoint: " + mountPoint + ". Use :fs mount {topology-url} {mountpoint-name}"; } - else { - return "No session established for mountPoint: " + mountPoint + " Use :fs mount {topology-url} {mountpoint-name}"; - } - } - else { - System.out.println("Unknown filesystem command"); - System.out.println(getUsage()); + } else { + terminal.writer().println("Unknown filesystem command: " + action); + terminal.writer().println(getUsage()); + terminal.writer().flush(); } return ""; } + private String printError(String msg) { + terminal.writer().println("Error: " + msg); + terminal.writer().flush(); + return null; + } + + // HELPER to safely extract filename + private String getFileName(String path) { + int index = path.lastIndexOf(File.separator); + return (index > -1) ? path.substring(index) : path; + } + private String get(String mountPoint, String from, String to) { - String result = null; try { Hdfs.get(sessions.get(mountPoint)).from(from).file(to).now().getString(); - result = "Successfully copied: " + from + " to: " + to; + return "Successfully copied: " + from + " to: " + to; } catch (KnoxShellException | IOException e) { - e.printStackTrace(); - result = "Exception ocurred: " + e.getMessage(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); + return "Exception occurred: " + e.getMessage(); } - return result; } private String mkdir(Map mounts, String path, String perms) { - String result = null; String mountPoint = determineMountPoint(path); KnoxSession session = getSessionForMountPoint(mounts, mountPoint); if (session != null) { @@ -166,45 +169,37 @@ private String mkdir(Map mounts, String path, String perms) { if (!exists(session, targetPath)) { try { if (perms != null) { - Hdfs.mkdir(sessions.get(mountPoint)).dir(targetPath).now().getString(); - } - else { - Hdfs.mkdir(session).dir(targetPath).perm(perms).now().getString(); + Hdfs.mkdir(sessions.get(mountPoint)).dir(targetPath).perm(perms).now().getString(); + } else { + Hdfs.mkdir(session).dir(targetPath).now().getString(); } - result = "Successfully created directory: " + targetPath; + return "Successfully created directory: " + targetPath; } catch (KnoxShellException | IOException e) { - e.printStackTrace(); - result = "Exception ocurred: " + e.getMessage(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); + return "Exception occurred: " + e.getMessage(); } + } else { + return targetPath + " already exists"; } - else { - result = targetPath + " already exists"; - } - } - else { - result = "No session established for mountPoint: " + mountPoint + " Use :fs mount {topology-url} {mountpoint-name}"; } - return result; + return "No session established for mountPoint: " + mountPoint; } private String cat(Map mounts, String path) { - String response = null; String mountPoint = determineMountPoint(path); KnoxSession session = getSessionForMountPoint(mounts, mountPoint); if (session != null) { String targetPath = determineTargetPath(path, mountPoint); try { - String contents = Hdfs.get(session).from(targetPath).now().getString(); - response = contents; + return Hdfs.get(session).from(targetPath).now().getString(); } catch (KnoxShellException | IOException e) { - e.printStackTrace(); - response = "Exception ocurred: " + e.getMessage(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); + return "Exception occurred: " + e.getMessage(); } } - else { - response = "No session established for mountPoint: " + mountPoint + " Use :fs mount {topology-url} {mountpoint-name}"; - } - return response; + return "No session established for mountPoint: " + mountPoint; } private String remove(Map mounts, String path) { @@ -215,11 +210,11 @@ private String remove(Map mounts, String path) { try { Hdfs.rm(session).file(targetPath).now().getString(); } catch (KnoxShellException | IOException e) { - e.printStackTrace(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } - } - else { - return "No session established for mountPoint: " + mountPoint + " Use :fs mount {topology-url} {mountpoint-name}"; + } else { + return "No session established for mountPoint: " + mountPoint; } return "Successfully removed: " + path; } @@ -232,70 +227,68 @@ private String put(Map mounts, String localFile, String path, in try { boolean overwrite = false; if (exists(session, targetPath)) { - if (collectClearInput(targetPath + " already exists would you like to overwrite (Y/n)").equalsIgnoreCase("y")) { + //Replaced System.console() with JLine 3 input + String answer = collectClearInput(targetPath + " already exists. Would you like to overwrite? (Y/n): "); + if (answer != null && answer.trim().equalsIgnoreCase("y")) { overwrite = true; + } else { + return "Put operation cancelled."; } } Hdfs.put(session).file(localFile).to(targetPath).overwrite(overwrite).permission(permission).now().getString(); } catch (IOException e) { - e.printStackTrace(); - return "Exception ocurred: " + e.getMessage(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); + return "Exception occurred: " + e.getMessage(); } - } - else { - return "No session established for mountPoint: " + mountPoint + " Use :fs mount {topology-url} {mountpoint-name}"; + } else { + return "No session established for mountPoint: " + mountPoint; } return "Successfully put: " + localFile + " to: " + path; } private boolean exists(KnoxSession session, String path) { - boolean rc = false; try { Response response = Hdfs.status(session).file(path).now(); - rc = response.exists(); + return response.exists(); } catch (KnoxShellException e) { - // NOP + return false; } - return rc; } private Object listStatus(Map mounts, String path) { - Object response = null; try { - String directory; String mountPoint = determineMountPoint(path); if (mountPoint != null) { KnoxSession session = getSessionForMountPoint(mounts, mountPoint); if (session != null) { - directory = determineTargetPath(path, mountPoint); + String directory = determineTargetPath(path, mountPoint); String json = Hdfs.ls(session).dir(directory).now().getString(); - Map>>> map = - JsonUtils.getFileStatusesAsMap(json); - if (map != null) { + + Map>>> map = JsonUtils.getFileStatusesAsMap(json); + if (map != null && map.containsKey("FileStatuses")) { ArrayList> list = map.get("FileStatuses").get("FileStatus"); - KnoxShellTable table = buildTableFromListStatus(directory, list); - response = table; + return buildTableFromListStatus(directory, list); } + } else { + return "No session established for mountPoint: " + mountPoint; } - else { - response = "No session established for mountPoint: " + mountPoint + " Use :fs mount {topology-url} {mountpoint-name}"; - } - } - else { - response = "No mountpoint found. Use ':fs mount {topologyURL} {mountpoint}'."; + } else { + return "No mountpoint found. Use ':fs mount {topologyURL} {mountpoint}'."; } } catch (KnoxShellException | IOException e) { - response = "Exception ocurred: " + e.getMessage(); - e.printStackTrace(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); + return "Exception occurred: " + e.getMessage(); } - return response; + return null; } private KnoxShellTable listMounts(Map mounts) { KnoxShellTable table = new KnoxShellTable(); table.header("Mount Point").header("Topology URL"); - for (String mountPoint : mounts.keySet()) { - table.row().value(mountPoint).value(mounts.get(mountPoint)); + for (Map.Entry entry : mounts.entrySet()) { + table.row().value(entry.getKey()).value(entry.getValue()); } return table; } @@ -332,31 +325,31 @@ private KnoxSession establishSession(String mountPoint, String url) { try { dlg = login(); } catch (CredentialCollectionException e) { - e.printStackTrace(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); return null; } String username = dlg.name(); String password = new String(dlg.chars()); - KnoxSession session = null; try { - session = KnoxSession.login(url, username, password); + KnoxSession session = KnoxSession.login(url, username, password); sessions.put(mountPoint, session); + return session; } catch (URISyntaxException e) { - e.printStackTrace(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); + return null; } - return session; } + // Safely prompt for input using JLine 3 private String collectClearInput(String prompt) { - Console c = System.console(); - if (c == null) { - System.err.println("No console."); - System.exit(1); + try { + LineReader reader = LineReaderBuilder.builder().terminal(terminal).build(); + return reader.readLine(prompt); + } catch (Exception e) { + return ""; // Fallback gracefully if interrupted } - - String value = c.readLine(prompt); - - return value; } private String determineTargetPath(String path, String mountPoint) { @@ -373,14 +366,14 @@ private String stripMountPoint(String path, String mountPoint) { } private String determineMountPoint(String path) { - String mountPoint = null; - if (path.startsWith("/")) { - // does the user supplied path starts at a root - // if so check for a mountPoint based on the first element of the path + if (path != null && path.startsWith("/")) { String[] pathElements = path.split("/"); - mountPoint = pathElements[1]; + // Prevent array bounds exception + if (pathElements.length > 1) { + return pathElements[1]; + } } - return mountPoint; + return null; } private KnoxShellTable buildTableFromListStatus(String directory, List> list) { @@ -394,32 +387,43 @@ private KnoxShellTable buildTableFromListStatus(String directory, List map : list) { - cal.setTimeInMillis(Long.parseLong(map.get("modificationTime"))); - table.row() + if (list != null) { + for (Map map : list) { + cal.setTimeInMillis(Long.parseLong(map.get("modificationTime"))); + table.row() .value(map.get("permission")) .value(map.get("owner")) .value(map.get("group")) .value(map.get("length")) .value(cal.getTime()) .value(map.get("pathSuffix")); + } } - return table; } protected Map getMountPoints() { - Map mounts = null; try { - mounts = KnoxSession.loadMountPoints(); + return KnoxSession.loadMountPoints(); } catch (IOException e) { - e.printStackTrace(); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } - return mounts; + return null; } public static void main(String[] args) { - WebHDFSCommand cmd = new WebHDFSCommand(new Groovysh()); - cmd.execute(new ArrayList<>(Arrays.asList(args))); + try { + Terminal terminal = TerminalBuilder.builder().system(true).build(); + GroovyEngine engine = new GroovyEngine(); + WebHDFSCommand cmd = new WebHDFSCommand(engine, terminal); + Object result = cmd.execute(new ArrayList<>(Arrays.asList(args))); + if (result != null) { + terminal.writer().println(result); + terminal.writer().flush(); + } + } catch (Exception e) { + e.printStackTrace(); + } } -} +} \ No newline at end of file From e815c3ca39fa5398e5e5ae9ff672d0f7d7fab08f Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:05:29 +0100 Subject: [PATCH 04/40] KNOX-3278: fix checkstyle errors and logging configuration in maven-shade-plugin and knoxshell-log4j. Update aspectj for JDK 17. --- .../home/conf/knoxshell-log4j2.xml | 1 - gateway-shell-release/pom.xml | 1 + .../org/apache/knox/gateway/shell/Shell.java | 36 ++++++++++++++++++- .../commands/AbstractKnoxShellCommand.java | 5 +-- .../commands/AbstractSQLCommandSupport.java | 16 ++++----- .../gateway/shell/commands/CSVCommand.java | 1 - .../shell/commands/DataSourceCommand.java | 24 ++++--------- .../gateway/shell/commands/LoginCommand.java | 1 - .../shell/commands/WebHDFSCommand.java | 18 +++++----- pom.xml | 2 +- 10 files changed, 61 insertions(+), 44 deletions(-) diff --git a/gateway-shell-release/home/conf/knoxshell-log4j2.xml b/gateway-shell-release/home/conf/knoxshell-log4j2.xml index c8ed33c6e6..b8997b0302 100644 --- a/gateway-shell-release/home/conf/knoxshell-log4j2.xml +++ b/gateway-shell-release/home/conf/knoxshell-log4j2.xml @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - logs diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml index 31d47caef3..3c44f90a1d 100644 --- a/gateway-shell-release/pom.xml +++ b/gateway-shell-release/pom.xml @@ -58,6 +58,7 @@ schema/** **/*.ldif + META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 4ff41d2ea7..d35276a805 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -143,10 +143,44 @@ private static void startInteractiveShell() throws Exception { String trimmed = line.trim(); if (trimmed.isEmpty()) continue; - if (trimmed.equalsIgnoreCase(":exit") || trimmed.equalsIgnoreCase(":quit")) { + // --- BUILT-IN COMMANDS --- + if (":exit".equalsIgnoreCase(trimmed) || ":quit".equalsIgnoreCase(trimmed)) { break; } + if (trimmed.startsWith(":help") || trimmed.startsWith(":h")) { + String[] helpParts = trimmed.split("\\s+"); + + if (helpParts.length > 1) { + // Detailed help for a specific command (e.g., ":help :fs") + String targetCmd = helpParts[1]; + if (registry.containsKey(targetCmd)) { + AbstractKnoxShellCommand cmd = registry.get(targetCmd); + terminal.writer().println(cmd.getDescription()); + terminal.writer().println(cmd.getUsage()); + } else { + terminal.writer().println("Unknown command: " + targetCmd); + } + } else { + // General help menu + terminal.writer().println("Available Custom Knox Commands:"); + + // Use a Stream to get distinct commands (ignores duplicate alias keys) + registry.values().stream().distinct().forEach(cmd -> { + String names = cmd.getName() + (cmd.getShortcut() != null ? ", " + cmd.getShortcut() : ""); + String desc = cmd.getDescription() != null ? cmd.getDescription() : ""; + terminal.writer().printf(" %-25s %s%n", names, desc); + }); + + terminal.writer().println(); + terminal.writer().printf(" %-25s %s%n", ":help, :h", "Displays this help message or specific command usage"); + terminal.writer().printf(" %-25s %s%n", ":exit, :quit", "Exits the shell"); + terminal.writer().println("\nNote: Any other input is evaluated natively as Groovy code."); + } + terminal.writer().flush(); + continue; // Skip the rest of the loop + } + // Route custom Knox commands String[] parts = trimmed.split("\\s+"); String commandName = parts[0]; diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java index 099e4ce779..899fb42d6d 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java @@ -28,10 +28,7 @@ import org.jline.terminal.Terminal; public abstract class AbstractKnoxShellCommand { - static final String KNOXSQLHISTORY = "__knoxsqlhistory"; - protected static final String KNOXDATASOURCES = "__knoxdatasources"; - // NEW FIELDS: Holding the modern execution context protected final GroovyEngine engine; protected final Terminal terminal; private final String name; @@ -146,7 +143,7 @@ public String string() { @Override public byte[] bytes() { - return new String(collectedPassword).getBytes(java.nio.charset.StandardCharsets.UTF_8); + return new String(collectedPassword).getBytes(StandardCharsets.UTF_8); } @Override public String type() { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java index 7e407d10ad..4ecf9d5c4e 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java @@ -34,13 +34,11 @@ public abstract class AbstractSQLCommandSupport extends AbstractKnoxShellCommand { + protected static final String KNOXDATASOURCES = "__knoxdatasources"; protected static final String KNOXDATASOURCE = "__knoxdatasource"; - private static final Object KNOXDATASOURCE_CONNECTIONS = "__knoxdatasourceconnections"; - - public AbstractSQLCommandSupport(GroovyEngine engine, Terminal terminal, String name, String shortcut) { - super(engine, terminal, name, shortcut); - } + private static final String KNOXSQLHISTORY = "__knoxsqlhistory"; + private static final String KNOXDATASOURCE_CONNECTIONS = "__knoxdatasourceconnections"; public AbstractSQLCommandSupport(GroovyEngine engine, Terminal terminal, String name, String shortcut, String desc, String usage, String help) { @@ -50,7 +48,7 @@ public AbstractSQLCommandSupport(GroovyEngine engine, Terminal terminal, String @SuppressWarnings("unchecked") protected Connection getConnectionFromSession(KnoxDataSource ds) { //GroovyEngine bindings lack getOrDefault, so we check for null manually - HashMap connections = (HashMap) engine.get((String)KNOXDATASOURCE_CONNECTIONS); + HashMap connections = (HashMap) engine.get(KNOXDATASOURCE_CONNECTIONS); if (connections == null) { connections = new HashMap<>(); } @@ -58,7 +56,7 @@ protected Connection getConnectionFromSession(KnoxDataSource ds) { } @SuppressWarnings("unchecked") - protected Connection getConnection(KnoxDataSource ds, String user, String pass) throws SQLException, Exception { + protected Connection getConnection(KnoxDataSource ds, String user, String pass) throws SQLException { Connection conn = getConnectionFromSession(ds); if (conn == null) { if (user != null && pass != null) { @@ -67,12 +65,12 @@ protected Connection getConnection(KnoxDataSource ds, String user, String pass) conn = JDBCUtils.createConnection(ds.getConnectStr(), null, null); } - HashMap connections = (HashMap) engine.get((String) KNOXDATASOURCE_CONNECTIONS); + HashMap connections = (HashMap) engine.get(KNOXDATASOURCE_CONNECTIONS); if (connections == null) { connections = new HashMap<>(); } connections.put(ds.getName(), conn); - engine.put((String) KNOXDATASOURCE_CONNECTIONS, connections); + engine.put(KNOXDATASOURCE_CONNECTIONS, connections); } return conn; } diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java index b8cbcb78b2..87ba7b9e56 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java @@ -36,7 +36,6 @@ public CSVCommand(GroovyEngine engine, Terminal terminal) { @Override public Object execute(List args) { - // FIXED: Prevent shell crash if user types :csv with no arguments if (args == null || args.isEmpty()) { terminal.writer().println("Usage: " + USAGE); terminal.writer().flush(); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index efa8a61f3f..a23de30bd0 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -28,7 +28,6 @@ import org.apache.knox.gateway.shell.KnoxDataSource; import org.apache.knox.gateway.shell.table.KnoxShellTable; -// NEW IMPORTS: Replacing Groovysh import org.apache.groovy.groovysh.jline.GroovyEngine; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; @@ -42,16 +41,14 @@ public DataSourceCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, ":datasources", ":ds", DESC, USAGE, DESC); } - @SuppressWarnings({"unchecked", "PMD.CloseResource"}) + @SuppressWarnings({"PMD.CloseResource"}) @Override public Object execute(List args) { Map dataSources = getDataSources(); - // FIXED: Safely default to "list" without mutating the potentially immutable args list String action = (args == null || args.isEmpty()) ? "list" : args.get(0); - if (action.equalsIgnoreCase("add")) { - // FIXED: Prevent IndexOutOfBoundsException + if ("add".equalsIgnoreCase(action)) { if (args.size() < 5) { terminal.writer().println("Error: Missing arguments for 'add'."); terminal.writer().println("Usage: :ds add ds-name connection-str driver-classname authntype"); @@ -63,11 +60,10 @@ public Object execute(List args) { engine.put(KNOXDATASOURCES, dataSources); // REFACTORED: Use engine.put persistDataSources(); } - else if (action.equalsIgnoreCase("remove")) { + else if ("remove".equalsIgnoreCase(action)) { if (dataSources == null || dataSources.isEmpty()) { return "No datasources to remove."; } - // FIXED: Prevent IndexOutOfBoundsException if (args.size() < 2) { terminal.writer().println("Error: Missing datasource name to remove."); terminal.writer().flush(); @@ -77,9 +73,8 @@ else if (action.equalsIgnoreCase("remove")) { String dsName = args.get(1); dataSources.remove(dsName); - // REFACTORED: Use engine.get() and engine.put() if (engine.get(KNOXDATASOURCE) != null) { - if (((String) engine.get(KNOXDATASOURCE)).equals(dsName)) { + if ((engine.get(KNOXDATASOURCE)).equals(dsName)) { terminal.writer().println("Unselecting datasource."); terminal.writer().flush(); engine.put(KNOXDATASOURCE, ""); @@ -87,15 +82,12 @@ else if (action.equalsIgnoreCase("remove")) { } engine.put(KNOXDATASOURCES, dataSources); persistDataSources(); - } - else if (action.equalsIgnoreCase("list")) { + } else if ("list".equalsIgnoreCase(action)) { // valid command no additional work needed though - } - else if (action.equalsIgnoreCase("select")) { + } else if ("select".equalsIgnoreCase(action)) { if (dataSources == null || dataSources.isEmpty()) { return "No datasources to select from."; } - // FIXED: Prevent IndexOutOfBoundsException if (args.size() < 2) { terminal.writer().println("Error: Missing datasource name to select."); terminal.writer().flush(); @@ -126,7 +118,6 @@ else if (action.equalsIgnoreCase("select")) { pass = dlg.chars(); } try { - // FIXED: Prevent NullPointerException if pass is null String passStr = (pass == null) ? null : new String(pass); getConnection(dsValue, username, passStr); } catch (Exception e) { @@ -150,8 +141,7 @@ else if (action.equalsIgnoreCase("select")) { datasource.header("Name").header("Connect String").header("Driver").header("Authn Type"); datasource.row().value(dsValue.getName()).value(dsValue.getConnectStr()).value(dsValue.getDriver()).value(dsValue.getAuthnType()); return datasource; - } - else { + } else { return "ERROR: unknown datasources command: " + action; } diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java index 77e5c9da96..21a91592c5 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java @@ -42,7 +42,6 @@ public LoginCommand(GroovyEngine engine, Terminal terminal) { @Override public Object execute(List args) { - // FIXED: Prevent IndexOutOfBounds if user types :login without a URL if (args == null || args.isEmpty()) { terminal.writer().println("Error: Knox Gateway URL required."); terminal.writer().println(getUsage()); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java index 5f3e4d7f2a..c5b85e5a07 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java @@ -73,23 +73,23 @@ public Object execute(List args) { String action = (args == null || args.isEmpty()) ? "ls" : args.get(0); - if (action.equalsIgnoreCase("mount")) { + if ("mount".equalsIgnoreCase(action)) { if (args.size() < 3) return printError("Usage: :fs mount "); return mount(mounts, args.get(1), args.get(2)); } - else if (action.equalsIgnoreCase("unmount")) { + else if ("unmount".equalsIgnoreCase(action)) { if (args.size() < 2) return printError("Usage: :fs unmount "); unmount(mounts, args.get(1)); return "Unmounted " + args.get(1); } - else if (action.equalsIgnoreCase("mounts")) { + else if ("mounts".equalsIgnoreCase(action)) { return listMounts(mounts); } - else if (action.equalsIgnoreCase("ls")) { + else if ("ls".equalsIgnoreCase(action)) { if (args.size() < 2) return printError("Usage: :fs ls "); return listStatus(mounts, args.get(1)); } - else if (action.equalsIgnoreCase("put")) { + else if ("put".equalsIgnoreCase(action)) { if (args.size() < 3) return printError("Usage: :fs put [permissions]"); String localFile = args.get(1); String path = args.get(2); @@ -103,20 +103,20 @@ else if (action.equalsIgnoreCase("put")) { } return put(mounts, localFile, path, permission); } - else if (action.equalsIgnoreCase("rm")) { + else if ("rm".equalsIgnoreCase(action)) { if (args.size() < 2) return printError("Usage: :fs rm "); return remove(mounts, args.get(1)); } - else if (action.equalsIgnoreCase("cat")) { + else if ("cat".equalsIgnoreCase(action)) { if (args.size() < 2) return printError("Usage: :fs cat "); return cat(mounts, args.get(1)); } - else if (action.equalsIgnoreCase("mkdir")) { + else if ("mkdir".equalsIgnoreCase(action)) { if (args.size() < 2) return printError("Usage: :fs mkdir [perms]"); String perms = (args.size() == 3) ? args.get(2) : null; return mkdir(mounts, args.get(1), perms); } - else if (action.equalsIgnoreCase("get")) { + else if ("get".equalsIgnoreCase(action)) { if (args.size() < 2) return printError("Usage: :fs get [to-path]"); String path = args.get(1); String mountPoint = determineMountPoint(path); diff --git a/pom.xml b/pom.xml index 8922ce8019..e3b2ad5973 100644 --- a/pom.xml +++ b/pom.xml @@ -167,7 +167,7 @@ 0.13 1.8.1 9.0 - 1.9.6 + 1.9.25.1 4.1.5 1.79 1.79 From 9d2c76f417e0a0ddccfa2a914d3729753c9f2585 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:49:53 +0100 Subject: [PATCH 05/40] KNOX-3278: Fix forbiddenapis check: use Locale.ROOT in printf. --- .../src/main/java/org/apache/knox/gateway/shell/Shell.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index d35276a805..3b25101720 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -48,6 +48,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -169,12 +170,12 @@ private static void startInteractiveShell() throws Exception { registry.values().stream().distinct().forEach(cmd -> { String names = cmd.getName() + (cmd.getShortcut() != null ? ", " + cmd.getShortcut() : ""); String desc = cmd.getDescription() != null ? cmd.getDescription() : ""; - terminal.writer().printf(" %-25s %s%n", names, desc); + terminal.writer().printf(Locale.ROOT, " %-25s %s%n", names, desc); }); terminal.writer().println(); - terminal.writer().printf(" %-25s %s%n", ":help, :h", "Displays this help message or specific command usage"); - terminal.writer().printf(" %-25s %s%n", ":exit, :quit", "Exits the shell"); + terminal.writer().printf(Locale.ROOT, " %-25s %s%n", ":help, :h", "Displays this help message or specific command usage"); + terminal.writer().printf(Locale.ROOT, " %-25s %s%n", ":exit, :quit", "Exits the shell"); terminal.writer().println("\nNote: Any other input is evaluated natively as Groovy code."); } terminal.writer().flush(); From 58f87409cfd5340e1a7ceda4913c8865c9e04bb6 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:15:18 +0100 Subject: [PATCH 06/40] KNOX-3278: Fix KnoxShellTableCallHistoryTest shouldRollbackToValidPreviousStep --- .../knox/gateway/shell/table/KnoxShellTable.java | 12 +++++++++--- .../shell/table/KnoxShellTableCallHistory.java | 8 ++++++++ .../table/KnoxShellTableCallHistoryTest.java | 15 ++++++++++++++- .../gateway/shell/table/KnoxShellTableTest.java | 12 ++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java index d83fc3a6d7..99d89a191f 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java @@ -22,7 +22,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.SortOrder; @@ -292,7 +291,7 @@ public KnoxShellTable apply(KnoxShellTableCell { + long now = System.currentTimeMillis(); + // If we are moving too fast, artificially step forward by 1ms to avoid collision + return (now > lastTime) ? now : lastTime + 1; + }); } public List getCallHistoryList() { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistory.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistory.java index 77451de541..5cfa125d65 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistory.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistory.java @@ -66,6 +66,14 @@ void removeCallsById(long id) { callHistory.remove(id); } + /** + * Clears the entire call history. + * Useful for ensuring clean state between unit tests. + */ + void clear() { + callHistory.clear(); + } + public List getCallHistory(long id) { return callHistory.containsKey(id) ? Collections.unmodifiableList(callHistory.get(id)) : Collections.emptyList(); } diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java index edb9f2f518..e143032173 100644 --- a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java +++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java @@ -26,6 +26,8 @@ import java.util.LinkedList; import java.util.List; +import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -49,6 +51,17 @@ public static void init() { CALL_LIST.add(new KnoxShellTableCall("org.apache.knox.gateway.shell.table.KnoxShellTableFilter", "greaterThan", true, Collections.singletonMap("5", String.class))); } + @Before + public void setUp() { + KnoxShellTableCallHistory.getInstance().clear(); + } + + @After + public void tearDown() { + KnoxShellTableCallHistory.getInstance().clear(); + } + + @Test public void shouldReturnEmptyListInCaseThereWasNoCall() throws Exception { final long id = KnoxShellTable.getUniqueTableId(); @@ -122,7 +135,7 @@ public void shouldRollbackToValidPreviousStep() throws Exception { table.rollback(); assertNotNull(table); assertEquals(14, table.rows.size()); - assertEquals(table.values(0).get(13), "14"); // selected the first column (ZIP) where the last element - index 13 - is 14 + assertEquals("14", table.values(0).get(13)); } private void recordCallHistory(long id, int steps) { diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java index 5835dbe45c..b73e20e487 100644 --- a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java +++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java @@ -55,6 +55,8 @@ import org.apache.knox.gateway.shell.jdbc.Database; import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase; import org.easymock.IAnswer; +import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -67,6 +69,16 @@ public class KnoxShellTableTest { private static final String SYSTEM_PROPERTY_DERBY_STREAM_ERROR_FILE = "derby.stream.error.file"; private static final String SAMPLE_DERBY_DATABASE_NAME = "sampleDerbyDatabase"; + @Before + public void setUp() { + KnoxShellTableCallHistory.getInstance().clear(); + } + + @After + public void tearDown() { + KnoxShellTableCallHistory.getInstance().clear(); + } + @Test public void testSimpleTableRendering() { String expectedResult = "+------------+------------+------------+\n" From 44dfac38835a06d3bd3b1e4457e56f4f94c92723 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:44:53 +0100 Subject: [PATCH 07/40] KNOX-3278: Removing comments and unused methods --- .../shell/commands/AbstractKnoxShellCommand.java | 15 ++------------- .../knox/gateway/shell/commands/CSVCommand.java | 5 ----- .../gateway/shell/commands/DataSourceCommand.java | 7 +++---- .../knox/gateway/shell/commands/LoginCommand.java | 2 -- .../gateway/shell/commands/SelectCommand.java | 4 +--- .../gateway/shell/commands/WebHDFSCommand.java | 1 - 6 files changed, 6 insertions(+), 28 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java index 899fb42d6d..af7a84ab88 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java @@ -38,15 +38,6 @@ public abstract class AbstractKnoxShellCommand { private String usage; private String help; - // REFACTORED CONSTRUCTOR: Injects JLine 3 dependencies - public AbstractKnoxShellCommand(GroovyEngine engine, Terminal terminal, String name, String shortcut) { - this.engine = engine; - this.terminal = terminal; - this.name = name; - this.shortcut = shortcut; - } - - // REFACTORED CONSTRUCTOR: Overload with help docs public AbstractKnoxShellCommand(GroovyEngine engine, Terminal terminal, String name, String shortcut, String desc, String usage, String help) { this.engine = engine; @@ -58,7 +49,7 @@ public AbstractKnoxShellCommand(GroovyEngine engine, Terminal terminal, String n this.help = help; } - // NEW METHODS: Exposing the command identifiers since CommandSupport is gone + // Exposing the command identifiers since CommandSupport is gone public String getName() { return name; } public String getShortcut() { return shortcut; } @@ -74,7 +65,7 @@ public String getHelp() { return help; } - // NEW ABSTRACT METHOD: Enforces the execution contract for subclasses + // Enforces the execution contract for subclasses public abstract Object execute(List args) throws Exception; protected String getBindingVariableNameForResultingTable(List args) { @@ -152,12 +143,10 @@ public String type() { @Override public void setPrompt(String prompt) { - } @Override public void setName(String name) { - } }; } diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java index 87ba7b9e56..afb3b0ece7 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java @@ -29,7 +29,6 @@ public class CSVCommand extends AbstractKnoxShellCommand { private static final String USAGE = ":csv [withHeaders] file-url||$variable-name [assign resulting-variable-name]"; private static final String DESC = "Build table from CSV file located at provided URL or KnoxShell $variable-name"; - // REFACTORED CONSTRUCTOR: Takes engine and terminal instead of shell public CSVCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, ":CSV", ":csv", DESC, USAGE, DESC); } @@ -65,7 +64,6 @@ public Object execute(List args) { try { if (withHeaders) { if (url.startsWith("$")) { - // REFACTORED: Use engine.get() instead of getVariables().get() String csvString = (String) engine.get(url.substring(1)); table = KnoxShellTable.builder().csv().withHeaders().string(csvString); } else { @@ -73,7 +71,6 @@ public Object execute(List args) { } } else { if (url.startsWith("$")) { - // REFACTORED: Use engine.get() instead of getVariables().get() String csvString = (String) engine.get(url.substring(1)); table = KnoxShellTable.builder().csv().string(csvString); } else { @@ -81,14 +78,12 @@ public Object execute(List args) { } } } catch (IOException e) { - // REFACTORED: Print errors nicely via the JLine 3 terminal terminal.writer().println("Error parsing CSV: " + e.getMessage()); e.printStackTrace(terminal.writer()); terminal.writer().flush(); } if (table != null && bindVariableName != null) { - // REFACTORED: Use engine.put() instead of getVariables().put() engine.put(bindVariableName, table); terminal.writer().println("Assigned resulting table to variable: " + bindVariableName); terminal.writer().flush(); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index a23de30bd0..4edd22acad 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -36,7 +36,6 @@ public class DataSourceCommand extends AbstractSQLCommandSupport { private static final String USAGE = ":ds (add|remove|list|select) [ds-name] [connection-str] [driver-classname] [authntype(none|basic)]"; private static final String DESC = "Datasource management commands. Persisted datasources maintain connection details across sessions"; - // REFACTORED CONSTRUCTOR public DataSourceCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, ":datasources", ":ds", DESC, USAGE, DESC); } @@ -57,7 +56,7 @@ public Object execute(List args) { } KnoxDataSource ds = new KnoxDataSource(args.get(1), args.get(2), args.get(3), args.get(4)); dataSources.put(ds.getName(), ds); - engine.put(KNOXDATASOURCES, dataSources); // REFACTORED: Use engine.put + engine.put(KNOXDATASOURCES, dataSources); persistDataSources(); } else if ("remove".equalsIgnoreCase(action)) { @@ -133,7 +132,7 @@ else if ("remove".equalsIgnoreCase(action)) { } if (dataSources.containsKey(args.get(1))) { - engine.put(KNOXDATASOURCE, args.get(1)); // REFACTORED: Use engine.put + engine.put(KNOXDATASOURCE, args.get(1)); } KnoxShellTable datasource = new KnoxShellTable(); @@ -155,7 +154,7 @@ private KnoxShellTable buildTable() { @SuppressWarnings("unchecked") Map dataSources = - (Map) engine.get(KNOXDATASOURCES); // REFACTORED: Use engine.get + (Map) engine.get(KNOXDATASOURCES); if (dataSources != null && !dataSources.isEmpty()) { for (KnoxDataSource dsValue : dataSources.values()) { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java index 21a91592c5..8ae9fe06ae 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java @@ -31,7 +31,6 @@ public class LoginCommand extends AbstractKnoxShellCommand { - // REFACTORED CONSTRUCTOR public LoginCommand(GroovyEngine engine, Terminal terminal) { // Pass identifiers and docs up to AbstractKnoxShellCommand super(engine, terminal, ":login", ":lgn", @@ -53,7 +52,6 @@ public Object execute(List args) { KnoxSession session = null; try { - // REPLACED KnoxLoginDialog with JLine 3 native prompting LineReader reader = LineReaderBuilder.builder() .terminal(terminal) .build(); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java index cbe32e089d..f785004e6b 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java @@ -28,13 +28,11 @@ import org.apache.knox.gateway.shell.KnoxDataSource; import org.apache.knox.gateway.shell.table.KnoxShellTable; -// Replacing Swing/AWT and Groovysh import org.apache.groovy.groovysh.jline.GroovyEngine; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; import org.jline.terminal.Terminal; -// REMOVED: "implements KeyListener" - JLine 3 handles this natively! public class SelectCommand extends AbstractSQLCommandSupport { private static final String USAGE = ":sql [assign resulting-variable-name]"; private static final String DESC = "Build table from SQL ResultSet"; @@ -116,7 +114,7 @@ public Object execute(List args) { } if (table != null && bindVariableName != null) { - engine.put(bindVariableName, table); // REFACTORED: Use engine.put() + engine.put(bindVariableName, table); terminal.writer().println("Assigned resulting table to variable: " + bindVariableName); terminal.writer().flush(); } diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java index c5b85e5a07..de10e41c38 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java @@ -59,7 +59,6 @@ public class WebHDFSCommand extends AbstractKnoxShellCommand { private Map sessions = new HashMap<>(); - // REFACTORED CONSTRUCTOR public WebHDFSCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, ":filesystem", ":fs", DESC, USAGE, DESC); } From 0437c93513bf0c98ff90184179adfc28d41627e0 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:28:49 +0100 Subject: [PATCH 08/40] KNOX-3278: Fix missing newlines at end of file. --- .../apache/knox/gateway/shell/AbstractCredentialCollector.java | 2 +- .../gateway/shell/AbstractJavaConsoleCredentialCollector.java | 2 +- .../src/main/java/org/apache/knox/gateway/shell/HttpDelete.java | 2 +- .../src/main/java/org/apache/knox/gateway/shell/Shell.java | 2 +- .../knox/gateway/shell/commands/AbstractKnoxShellCommand.java | 2 +- .../knox/gateway/shell/commands/AbstractSQLCommandSupport.java | 2 +- .../java/org/apache/knox/gateway/shell/commands/CSVCommand.java | 2 +- .../apache/knox/gateway/shell/commands/DataSourceCommand.java | 2 +- .../org/apache/knox/gateway/shell/commands/LoginCommand.java | 2 +- .../org/apache/knox/gateway/shell/commands/SelectCommand.java | 2 +- .../org/apache/knox/gateway/shell/commands/WebHDFSCommand.java | 2 +- .../main/java/org/apache/knox/gateway/shell/hdfs/Rename.java | 2 +- .../java/org/apache/knox/gateway/shell/knox/token/GetTest.java | 2 +- .../org/apache/knox/gateway/shell/knox/token/TokenTest.java | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/AbstractCredentialCollector.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/AbstractCredentialCollector.java index 3d1f2590b1..922595cc40 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/AbstractCredentialCollector.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/AbstractCredentialCollector.java @@ -63,4 +63,4 @@ public String name() { return name; } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/AbstractJavaConsoleCredentialCollector.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/AbstractJavaConsoleCredentialCollector.java index 468b422fa9..6f8fa18c1e 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/AbstractJavaConsoleCredentialCollector.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/AbstractJavaConsoleCredentialCollector.java @@ -59,4 +59,4 @@ public boolean validate() { } return rc; } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/HttpDelete.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/HttpDelete.java index 6475f703d7..c5427dd450 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/HttpDelete.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/HttpDelete.java @@ -39,4 +39,4 @@ public HttpDelete(URI uri) { public String getMethod() { return org.apache.http.client.methods.HttpDelete.METHOD_NAME; } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 3b25101720..f1be2fcd4c 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -225,4 +225,4 @@ private static void registerCommand(Map regist registry.put(cmd.getShortcut(), cmd); } } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java index af7a84ab88..cba962a5fc 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java @@ -150,4 +150,4 @@ public void setName(String name) { } }; } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java index 4ecf9d5c4e..83e08c9b68 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java @@ -214,4 +214,4 @@ public void closeConnections() { } }); } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java index afb3b0ece7..fb5973887e 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java @@ -91,4 +91,4 @@ public Object execute(List args) { return table; } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index 4edd22acad..5a98bf34ac 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -180,4 +180,4 @@ public static void main(String[] args) { e.printStackTrace(); } } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java index 8ae9fe06ae..68fb3059ba 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java @@ -108,4 +108,4 @@ public static void main(String[] args) { e.printStackTrace(); } } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java index f785004e6b..f3a0e80f27 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java @@ -148,4 +148,4 @@ private String promptForSQL(String dsName) { return null; } } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java index de10e41c38..fdf3ac7eea 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java @@ -425,4 +425,4 @@ public static void main(String[] args) { e.printStackTrace(); } } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/hdfs/Rename.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/hdfs/Rename.java index cec30e540c..f7633af24d 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/hdfs/Rename.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/hdfs/Rename.java @@ -67,4 +67,4 @@ public static class Response extends EmptyResponse { super(response); } } -} \ No newline at end of file +} diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/knox/token/GetTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/knox/token/GetTest.java index 8778b75a14..87dd685392 100644 --- a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/knox/token/GetTest.java +++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/knox/token/GetTest.java @@ -76,4 +76,4 @@ private void testGetRequest(boolean setDoAsUser, String doAsUser) { verify(knoxSession); } -} \ No newline at end of file +} diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/knox/token/TokenTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/knox/token/TokenTest.java index d93357c436..a1849b54bc 100644 --- a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/knox/token/TokenTest.java +++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/knox/token/TokenTest.java @@ -221,4 +221,4 @@ private void testTokenLifecyle(AbstractTokenLifecycleRequest request, final Stri assertEquals(testToken, postData); } -} \ No newline at end of file +} From cf082ab624d124cb18472a3127013bc825496d67 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:29:43 +0100 Subject: [PATCH 09/40] KNOX-3278: Restoring SelectCommand to use Swing JTextArea for multiline edits. --- .../commands/AbstractKnoxShellCommand.java | 11 +- .../commands/AbstractSQLCommandSupport.java | 3 +- .../gateway/shell/commands/CSVCommand.java | 3 +- .../shell/commands/DataSourceCommand.java | 1 + .../gateway/shell/commands/LoginCommand.java | 4 +- .../gateway/shell/commands/SelectCommand.java | 161 +++++++++++------- .../shell/commands/WebHDFSCommand.java | 22 ++- .../table/KnoxShellTableCallHistoryTest.java | 2 +- 8 files changed, 132 insertions(+), 75 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java index cba962a5fc..2a2992c73b 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java @@ -49,9 +49,13 @@ public AbstractKnoxShellCommand(GroovyEngine engine, Terminal terminal, String n this.help = help; } - // Exposing the command identifiers since CommandSupport is gone - public String getName() { return name; } - public String getShortcut() { return shortcut; } + public String getName() { + return name; + } + + public String getShortcut() { + return shortcut; + } public String getDescription() { return description; @@ -65,7 +69,6 @@ public String getHelp() { return help; } - // Enforces the execution contract for subclasses public abstract Object execute(List args) throws Exception; protected String getBindingVariableNameForResultingTable(List args) { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java index 83e08c9b68..e7b43f8b55 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractSQLCommandSupport.java @@ -176,7 +176,7 @@ protected void addToSQLHistory(List sqlHistory, String sql) { @SuppressWarnings("unchecked") protected void removeFromSQLHistory(String dsName) { Map> sqlHistories = (Map>) engine.get(KNOXSQLHISTORY); - if (sqlHistories != null) { // Can be null + if (sqlHistories != null) { sqlHistories.remove(dsName); persistSQLHistory(); } @@ -191,7 +191,6 @@ protected Map getDataSources() { engine.put(KNOXDATASOURCES, datasources); } else { datasources = new HashMap<>(); - engine.put(KNOXDATASOURCES, datasources); // ADDED: Store the empty map to prevent repeated loading attempts } } return datasources; diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java index fb5973887e..4fedc07232 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/CSVCommand.java @@ -44,7 +44,6 @@ public Object execute(List args) { KnoxShellTable table = null; String bindVariableName = getBindingVariableNameForResultingTable(args); - // FIXED: Moved to local variables to prevent state leaking between executions boolean withHeaders = false; String url; @@ -64,6 +63,7 @@ public Object execute(List args) { try { if (withHeaders) { if (url.startsWith("$")) { + // a knoxshell variable is a csv file as a string String csvString = (String) engine.get(url.substring(1)); table = KnoxShellTable.builder().csv().withHeaders().string(csvString); } else { @@ -71,6 +71,7 @@ public Object execute(List args) { } } else { if (url.startsWith("$")) { + // a knoxshell variable is a csv file as a string String csvString = (String) engine.get(url.substring(1)); table = KnoxShellTable.builder().csv().string(csvString); } else { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index 5a98bf34ac..346014488f 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -70,6 +70,7 @@ else if ("remove".equalsIgnoreCase(action)) { } String dsName = args.get(1); + // if the removed datasource is currently selected, unselect it dataSources.remove(dsName); if (engine.get(KNOXDATASOURCE) != null) { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java index 68fb3059ba..e603092702 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoginCommand.java @@ -32,7 +32,6 @@ public class LoginCommand extends AbstractKnoxShellCommand { public LoginCommand(GroovyEngine engine, Terminal terminal) { - // Pass identifiers and docs up to AbstractKnoxShellCommand super(engine, terminal, ":login", ":lgn", "Establishes a Knox session", "Usage: :login ", @@ -91,12 +90,11 @@ public Object execute(List args) { terminal.writer().flush(); } - return session; // Returning the session object + return session; } public static void main(String[] args) { try { - // Test using JLine 3 Terminal Terminal terminal = TerminalBuilder.builder().system(true).build(); GroovyEngine engine = new GroovyEngine(); LoginCommand cmd = new LoginCommand(engine, terminal); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java index f3a0e80f27..67d8ecf61a 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/SelectCommand.java @@ -17,6 +17,8 @@ */ package org.apache.knox.gateway.shell.commands; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -29,23 +31,69 @@ import org.apache.knox.gateway.shell.table.KnoxShellTable; import org.apache.groovy.groovysh.jline.GroovyEngine; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; import org.jline.terminal.Terminal; -public class SelectCommand extends AbstractSQLCommandSupport { +import javax.swing.Box; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +public class SelectCommand extends AbstractSQLCommandSupport implements KeyListener { private static final String USAGE = ":sql [assign resulting-variable-name]"; private static final String DESC = "Build table from SQL ResultSet"; private static final String KNOXDATASOURCE = "__knoxdatasource"; + private JTextArea sqlField; + private List sqlHistory; + private int historyIndex = -1; public SelectCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, ":SQL", ":sql", DESC, USAGE, DESC); } - @SuppressWarnings({"unchecked", "PMD.CloseResource"}) + @Override + public void keyPressed(KeyEvent event) { + int code = event.getKeyCode(); + boolean setFromHistory = false; + if (sqlHistory != null && !sqlHistory.isEmpty()) { + if (historyIndex == -1) { + historyIndex = sqlHistory.size() + 1; + } + if (code == KeyEvent.VK_KP_UP || + code == KeyEvent.VK_UP) { + if (historyIndex > 0) { + historyIndex -= 1; + } + setFromHistory = true; + } + else if (code == KeyEvent.VK_KP_DOWN || + code == KeyEvent.VK_DOWN) { + if (historyIndex < sqlHistory.size() - 1) { + historyIndex += 1; + setFromHistory = true; + } + } + if (setFromHistory) { + sqlField.setText(sqlHistory.get(historyIndex)); + sqlField.invalidate(); + } + } + } + + @Override + public void keyReleased(KeyEvent event) { + } + + @Override + public void keyTyped(KeyEvent event) { + } + + @SuppressWarnings({"PMD.CloseResource"}) @Override public Object execute(List args) { + boolean ok = false; + String sql = ""; String bindVariableName = null; KnoxShellTable table = null; @@ -55,7 +103,7 @@ public Object execute(List args) { String dsName = (String) engine.get(KNOXDATASOURCE); Map dataSources = getDataSources(); - KnoxDataSource ds = null; + KnoxDataSource ds; if (dsName == null || dsName.isEmpty()) { if (dataSources == null || dataSources.isEmpty()) { @@ -68,43 +116,62 @@ public Object execute(List args) { } ds = dataSources.get(dsName); - if (ds != null) { - String sql = promptForSQL(dsName); + sqlHistory = getSQLHistory(dsName); + historyIndex = (sqlHistory != null && !sqlHistory.isEmpty()) ? sqlHistory.size() - 1 : -1; - if (sql == null || sql.trim().isEmpty()) { - return "Query cancelled or empty."; + if (ds != null) { + JLabel jl = new JLabel("Query: "); + sqlField = new JTextArea(5,40); + sqlField.addKeyListener(this); + sqlField.setLineWrap(true); + JScrollPane scrollPane = new JScrollPane(sqlField); + Box box = Box.createHorizontalBox(); + box.add(jl); + box.add(scrollPane); + + // JDK-5018574 : Unable to set focus to another component in JOptionPane + SwingUtils.workAroundFocusIssue(sqlField); + + int x = JOptionPane.showConfirmDialog(null, box, + "SQL Query Input", JOptionPane.OK_CANCEL_OPTION); + + if (x == JOptionPane.OK_OPTION) { + ok = true; + sql = sqlField.getText(); + addToSQLHistory(dsName, sql); + historyIndex = -1; } - addToSQLHistory(dsName, sql); + //KnoxShellTable.builder().jdbc().connect("jdbc:derby:codejava/webdb1").driver("org.apache.derby.jdbc.EmbeddedDriver").username("lmccay").pwd("xxxx").sql("SELECT * FROM book"); try { - terminal.writer().println("Executing: " + sql); - terminal.writer().flush(); - - Connection conn = getConnectionFromSession(ds); - if (conn == null || conn.isClosed()) { - String username = null; - char[] pass = null; - if ("basic".equalsIgnoreCase(ds.getAuthnType())) { - CredentialCollector dlg = login(); - username = dlg.name(); - pass = dlg.chars(); - } - // NullPointerException prevention for pass - String passStr = (pass == null) ? null : new String(pass); - conn = getConnection(ds, username, passStr); - } - - try (Statement statement = conn.createStatement()) { - if (statement.execute(sql)) { - try (ResultSet resultSet = statement.getResultSet()) { - table = KnoxShellTable.builder().jdbc().resultSet(resultSet); + if (ok) { + System.out.println(sql); + try { + Connection conn = getConnectionFromSession(ds); + if (conn == null || conn.isClosed()) { + String username = null; + char[] pass = null; + if (ds.getAuthnType().equalsIgnoreCase("basic")) { + CredentialCollector dlg = login(); + username = dlg.name(); + pass = dlg.chars(); + } + String passStr = (pass == null) ? null : new String(pass); + conn = getConnection(ds, username, passStr); } + try (Statement statement = conn.createStatement()) { + if (statement.execute(sql)) { + try (ResultSet resultSet = statement.getResultSet()) { + table = KnoxShellTable.builder().jdbc().resultSet(resultSet); + } + } + } + } catch (SQLException e) { + terminal.writer().println("SQL Exception encountered: " + e.getMessage()); + terminal.writer().flush(); } } - } catch (SQLException e) { - terminal.writer().println("SQL Exception encountered: " + e.getMessage()); - terminal.writer().flush(); } catch (Exception e) { e.printStackTrace(terminal.writer()); terminal.writer().flush(); @@ -122,30 +189,4 @@ public Object execute(List args) { return table; } - /** - * Replaces the old Swing JOptionPane and KeyListener with a native JLine 3 prompt. - */ - private String promptForSQL(String dsName) { - try { - // Build a temporary LineReader just for the SQL prompt - LineReader sqlReader = LineReaderBuilder.builder() - .terminal(terminal) - .build(); - - // Load the specific SQL history for this datasource into JLine - List sqlHistory = getSQLHistory(dsName); - if (sqlHistory != null) { - for (String pastQuery : sqlHistory) { - sqlReader.getHistory().add(pastQuery); - } - } - - // Prompt the user in the terminal (Up/Down arrows automatically cycle through the history we just added!) - return sqlReader.readLine("SQL (" + dsName + ")> "); - - } catch (org.jline.reader.UserInterruptException | org.jline.reader.EndOfFileException e) { - // User hit Ctrl+C or Ctrl+D to cancel the prompt - return null; - } - } } diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java index fdf3ac7eea..63348e4471 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java @@ -85,10 +85,15 @@ else if ("mounts".equalsIgnoreCase(action)) { return listMounts(mounts); } else if ("ls".equalsIgnoreCase(action)) { - if (args.size() < 2) return printError("Usage: :fs ls "); - return listStatus(mounts, args.get(1)); + if (args == null || args.size() < 2) { + return printError("Usage: :fs ls "); + } else { + return listStatus(mounts, args.get(1)); + } } else if ("put".equalsIgnoreCase(action)) { + // Hdfs.put( session ).file( dataFile ).to( dataDir + "/" + dataFile ).now() + // :fs put from-path to-path if (args.size() < 3) return printError("Usage: :fs put [permissions]"); String localFile = args.get(1); String path = args.get(2); @@ -103,19 +108,27 @@ else if ("put".equalsIgnoreCase(action)) { return put(mounts, localFile, path, permission); } else if ("rm".equalsIgnoreCase(action)) { + // Hdfs.rm( session ).file( dataFile ).now() + // :fs rm target-path if (args.size() < 2) return printError("Usage: :fs rm "); return remove(mounts, args.get(1)); } else if ("cat".equalsIgnoreCase(action)) { + // println Hdfs.get( session ).from( dataDir + "/" + dataFile ).now().string + // :fs cat target-path if (args.size() < 2) return printError("Usage: :fs cat "); return cat(mounts, args.get(1)); } else if ("mkdir".equalsIgnoreCase(action)) { + // println Hdfs.mkdir( session ).dir( directoryPath ).perm( "777" ).now().string + // :fs mkdir target-path [perms] if (args.size() < 2) return printError("Usage: :fs mkdir [perms]"); String perms = (args.size() == 3) ? args.get(2) : null; return mkdir(mounts, args.get(1), perms); } else if ("get".equalsIgnoreCase(action)) { + // println Hdfs.get( session ).from( dataDir + "/" + dataFile ).now().string + // :fs get from-path [to-path] if (args.size() < 2) return printError("Usage: :fs get [to-path]"); String path = args.get(1); String mountPoint = determineMountPoint(path); @@ -273,7 +286,7 @@ private Object listStatus(Map mounts, String path) { return "No session established for mountPoint: " + mountPoint; } } else { - return "No mountpoint found. Use ':fs mount {topologyURL} {mountpoint}'."; + return "No mountPoint found. Use ':fs mount {topologyURL} {mountPoint}'."; } } catch (KnoxShellException | IOException e) { e.printStackTrace(terminal.writer()); @@ -366,8 +379,9 @@ private String stripMountPoint(String path, String mountPoint) { private String determineMountPoint(String path) { if (path != null && path.startsWith("/")) { + // does the user supplied path starts at a root + // if so check for a mountPoint based on the first element of the path String[] pathElements = path.split("/"); - // Prevent array bounds exception if (pathElements.length > 1) { return pathElements[1]; } diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java index e143032173..1b99962f5c 100644 --- a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java +++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableCallHistoryTest.java @@ -135,7 +135,7 @@ public void shouldRollbackToValidPreviousStep() throws Exception { table.rollback(); assertNotNull(table); assertEquals(14, table.rows.size()); - assertEquals("14", table.values(0).get(13)); + assertEquals("14", table.values(0).get(13)); // selected the first column (ZIP) where the last element - index 13 - is 14 } private void recordCallHistory(long id, int steps) { From 2cb83ec3b3f6a814b024ca57227377ddbaef1563 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:57:01 +0100 Subject: [PATCH 10/40] KNOX-3278: Removing KnoxLoginDialog (JLine3 supports password input). --- .../shell/commands/KnoxLoginDialog.java | 111 ------------------ 1 file changed, 111 deletions(-) delete mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/KnoxLoginDialog.java diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/KnoxLoginDialog.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/KnoxLoginDialog.java deleted file mode 100644 index 90169e0964..0000000000 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/KnoxLoginDialog.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.knox.gateway.shell.commands; - -import javax.swing.Box; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPasswordField; -import javax.swing.JTextField; -import org.apache.knox.gateway.shell.CredentialCollectionException; -import org.apache.knox.gateway.shell.CredentialCollector; - -public class KnoxLoginDialog implements CredentialCollector { - public static final String COLLECTOR_TYPE = "LoginDialog"; - - public char[] pass; - public String username; - String name; - public boolean ok; - - @Override - public void collect() throws CredentialCollectionException { - JLabel jl = new JLabel("Enter Your username: "); - JTextField juf = new JTextField(24); - JLabel jl2 = new JLabel("Enter Your password: "); - JPasswordField jpf = new JPasswordField(24); - Box box1 = Box.createHorizontalBox(); - box1.add(jl); - box1.add(juf); - Box box2 = Box.createHorizontalBox(); - box2.add(jl2); - box2.add(jpf); - Box box = Box.createVerticalBox(); - box.add(box1); - box.add(box2); - - // JDK-5018574 : Unable to set focus to another component in JOptionPane - SwingUtils.workAroundFocusIssue(juf); - - int x = JOptionPane.showConfirmDialog(null, box, - "KnoxShell Login", JOptionPane.OK_CANCEL_OPTION); - - if (x == JOptionPane.OK_OPTION) { - ok = true; - username = juf.getText(); - pass = jpf.getPassword(); - } - } - - @Override - public String string() { - return new String(pass); - } - - @Override - public char[] chars() { - return pass; - } - - @Override - public byte[] bytes() { - return null; - } - - @Override - public String type() { - return "dialog"; - } - - @Override - public String name() { - return username; - } - - @Override - public void setPrompt(String prompt) { - } - - @Override - public void setName(String name) { - this.name = name; - } - - public static void main(String[] args) { - KnoxLoginDialog dlg = new KnoxLoginDialog(); - try { - dlg.collect(); - if (dlg.ok) { - System.out.println("username: " + dlg.username); - System.out.println("password: " + new String(dlg.pass)); - } - } catch (CredentialCollectionException e) { - e.printStackTrace(); - } - } -} From b26c621a40898c2b6c8811cc0afd967f8b3788f8 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:41:02 +0100 Subject: [PATCH 11/40] KNOX-3278: Correcting undeclared jline module dependencies. --- gateway-shell/pom.xml | 16 ++++++++++++---- pom.xml | 13 ++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/gateway-shell/pom.xml b/gateway-shell/pom.xml index 0070fd3f2a..42ec57e09f 100644 --- a/gateway-shell/pom.xml +++ b/gateway-shell/pom.xml @@ -62,10 +62,6 @@ org.apache.groovy groovy-json - - org.fusesource.jansi - jansi - org.apache.httpcomponents httpcore @@ -74,6 +70,18 @@ org.apache.httpcomponents httpclient + + org.jline + jansi + + + org.jline + jline-reader + + + org.jline + jline-terminal + net.minidev json-smart diff --git a/pom.xml b/pom.xml index e3b2ad5973..2d660fe2b0 100644 --- a/pom.xml +++ b/pom.xml @@ -223,7 +223,6 @@ 5.3.6 2.18.2 0.8.13 - 1.18 1.2.1 1.2.2 1.3.2 @@ -1653,14 +1652,18 @@ - org.fusesource.jansi + org.jline jansi - ${jansi.version} + ${jline.version} + + + org.jline + jline-reader + ${jline.version} - org.jline - jline + jline-terminal ${jline.version} From ebcaead718e5e74ce4bac94a4cdcc2afcf14e374 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:07:05 +0100 Subject: [PATCH 12/40] KNOX-3278: Correcting pmd findings. --- .../org/apache/knox/gateway/shell/Shell.java | 8 ++++-- .../shell/commands/WebHDFSCommand.java | 28 ++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index f1be2fcd4c..3b808ff62a 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -139,10 +139,14 @@ private static void startInteractiveShell() throws Exception { while (true) { try { String line = reader.readLine("knox> "); - if (line == null) break; + if (line == null) { + break; + } String trimmed = line.trim(); - if (trimmed.isEmpty()) continue; + if (trimmed.isEmpty()) { + continue; + } // --- BUILT-IN COMMANDS --- if (":exit".equalsIgnoreCase(trimmed) || ":quit".equalsIgnoreCase(trimmed)) { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java index 63348e4471..b1ca9e8199 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/WebHDFSCommand.java @@ -73,11 +73,15 @@ public Object execute(List args) { String action = (args == null || args.isEmpty()) ? "ls" : args.get(0); if ("mount".equalsIgnoreCase(action)) { - if (args.size() < 3) return printError("Usage: :fs mount "); + if (args.size() < 3) { + return printError("Usage: :fs mount "); + } return mount(mounts, args.get(1), args.get(2)); } else if ("unmount".equalsIgnoreCase(action)) { - if (args.size() < 2) return printError("Usage: :fs unmount "); + if (args.size() < 2) { + return printError("Usage: :fs unmount "); + } unmount(mounts, args.get(1)); return "Unmounted " + args.get(1); } @@ -94,7 +98,9 @@ else if ("ls".equalsIgnoreCase(action)) { else if ("put".equalsIgnoreCase(action)) { // Hdfs.put( session ).file( dataFile ).to( dataDir + "/" + dataFile ).now() // :fs put from-path to-path - if (args.size() < 3) return printError("Usage: :fs put [permissions]"); + if (args.size() < 3) { + return printError("Usage: :fs put [permissions]"); + } String localFile = args.get(1); String path = args.get(2); int permission = 755; @@ -110,26 +116,34 @@ else if ("put".equalsIgnoreCase(action)) { else if ("rm".equalsIgnoreCase(action)) { // Hdfs.rm( session ).file( dataFile ).now() // :fs rm target-path - if (args.size() < 2) return printError("Usage: :fs rm "); + if (args.size() < 2) { + return printError("Usage: :fs rm "); + } return remove(mounts, args.get(1)); } else if ("cat".equalsIgnoreCase(action)) { // println Hdfs.get( session ).from( dataDir + "/" + dataFile ).now().string // :fs cat target-path - if (args.size() < 2) return printError("Usage: :fs cat "); + if (args.size() < 2) { + return printError("Usage: :fs cat "); + } return cat(mounts, args.get(1)); } else if ("mkdir".equalsIgnoreCase(action)) { // println Hdfs.mkdir( session ).dir( directoryPath ).perm( "777" ).now().string // :fs mkdir target-path [perms] - if (args.size() < 2) return printError("Usage: :fs mkdir [perms]"); + if (args.size() < 2) { + return printError("Usage: :fs mkdir [perms]"); + } String perms = (args.size() == 3) ? args.get(2) : null; return mkdir(mounts, args.get(1), perms); } else if ("get".equalsIgnoreCase(action)) { // println Hdfs.get( session ).from( dataDir + "/" + dataFile ).now().string // :fs get from-path [to-path] - if (args.size() < 2) return printError("Usage: :fs get [to-path]"); + if (args.size() < 2) { + return printError("Usage: :fs get [to-path]"); + } String path = args.get(1); String mountPoint = determineMountPoint(path); KnoxSession session = getSessionForMountPoint(mounts, mountPoint); From ecb0f45009e4f63a9e019c10e6d7bfe56d8fa0df Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:47:52 +0100 Subject: [PATCH 13/40] KNOX-3278: Update rest-assured to 6.0.0 (needed for Groovy 5) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2d660fe2b0..7e9456b552 100644 --- a/pom.xml +++ b/pom.xml @@ -271,7 +271,7 @@ 2.0.9 0.0.11.1 0.12.4 - 5.5.6 + 6.0.0 1.13.0 1.13.0 1.2.6 From e914af02ef2fc434e037485042b095c49f1b3e8e Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:01:27 +0100 Subject: [PATCH 14/40] KNOX-3278: command completion pt1 --- .../org/apache/knox/gateway/shell/Shell.java | 64 +++++++++-- .../gateway/shell/SimpleCommandRegistry.java | 101 ++++++++++++++++++ .../commands/AbstractKnoxShellCommand.java | 7 ++ .../shell/commands/DataSourceCommand.java | 47 ++++++++ 4 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 3b808ff62a..855f949d2f 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -19,6 +19,7 @@ import groovy.ui.GroovyMain; +import org.apache.groovy.groovysh.jline.SystemRegistryImpl; import org.apache.knox.gateway.shell.commands.AbstractKnoxShellCommand; import org.apache.knox.gateway.shell.commands.CSVCommand; import org.apache.knox.gateway.shell.commands.DataSourceCommand; @@ -33,24 +34,31 @@ import org.apache.knox.gateway.shell.yarn.Yarn; import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.console.CommandMethods; +import org.jline.console.SystemRegistry; import org.jline.reader.Completer; import org.jline.reader.EndOfFileException; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; import org.jline.reader.UserInterruptException; +import org.jline.reader.impl.DefaultParser; import org.jline.reader.impl.completer.AggregateCompleter; +import org.jline.reader.impl.completer.NullCompleter; import org.jline.reader.impl.completer.StringsCompleter; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; public class Shell { @@ -111,20 +119,52 @@ private static void startInteractiveShell() throws Exception { registerCommand(registry, csvCmd); registerCommand(registry, hdfsCmd); - // 4. Setup Shutdown Hook (Calling closeConnections directly on our object instances) - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - System.out.println("\nClosing any open connections..."); - dsCmd.closeConnections(); - selectCmd.closeConnections(); - })); + Map commandMethods = new HashMap<>(); + Map commandAliases = new HashMap<>(); - // 5. Setup Tab Completers + + registry.forEach((name, cmd) -> { + String shortcut = cmd.getShortcut(); // Use your actual getter method here + if (shortcut != null && !shortcut.isEmpty()) { + commandAliases.put(shortcut, name); // e.g., "ds" -> "datasource" + } + + commandMethods.put(name, new CommandMethods( + // 1. Execution Logic: CommandInput -> String[] + (input) -> { + try { + // CommandInput.args() includes the command name as the first element + String[] allTokens = input.args(); + + // Convert to List and skip the first element (the command name) + List argsList = (allTokens != null && allTokens.length > 1) + ? Arrays.stream(allTokens).skip(1).collect(Collectors.toList()) + : Collections.emptyList(); + return cmd.execute(argsList); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + }, + // 2. Completion Logic: String (command name) -> Completer + (line) -> cmd.getCompleters() + )); + }); + + DefaultParser parser = new DefaultParser(); + Path workDir = Paths.get(System.getProperty("user.dir")); + SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null); + systemRegistry.register("knox", new SimpleCommandRegistry(commandMethods, commandAliases)); + systemRegistry.setCommandRegistries(); + SystemRegistry.add(systemRegistry); + + // 4. Setup Tab Completers // StringsCompleter automatically suggests our custom commands (e.g., ":sql", ":fs") Completer knoxCompleter = new StringsCompleter(registry.keySet()); Completer groovyCompleter = engine.getScriptCompleter(); Completer finalCompleter = new AggregateCompleter(knoxCompleter, groovyCompleter); - // 6. Build the LineReader + // 5. Build the LineReader LineReader reader = LineReaderBuilder.builder() .terminal(terminal) .completer(finalCompleter) @@ -135,6 +175,14 @@ private static void startInteractiveShell() throws Exception { terminal.writer().println("Type ':help' for help, ':exit' or ':quit' to quit."); terminal.writer().flush(); + // 6. Setup Shutdown Hook (Calling closeConnections directly on our object instances) + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\nClosing any open connections..."); + dsCmd.closeConnections(); + selectCmd.closeConnections(); + })); + + // 7. The REPL Loop while (true) { try { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java new file mode 100644 index 0000000000..6eb44cf4ba --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.shell; +import org.jline.console.CommandRegistry; +import org.jline.console.CommandMethods; +import org.jline.console.CommandInput; +import org.jline.console.CmdDesc; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.SystemCompleter; + +import java.util.*; + +public class SimpleCommandRegistry implements CommandRegistry { + + private final Map commands; + private final Map aliases; + + public SimpleCommandRegistry(Map commands, Map aliases) { + this.commands = commands; + this.aliases = aliases != null ? aliases : Collections.emptyMap(); + } + + @Override + public boolean hasCommand(String command) { + return commands.containsKey(command) || aliases.containsKey(command); + } + + @Override + public Set commandNames() { + return commands.keySet(); + } + + @Override + public Map commandAliases() { + return aliases; + } + + @Override + public List commandInfo(String command) { + return Collections.emptyList(); + } + + @Override + public CmdDesc commandDescription(List args) { + return new CmdDesc(false); // Disables floating tooltip widgets for these commands + } + + @Override + public SystemCompleter compileCompleters() { + SystemCompleter out = new SystemCompleter(); + + // Add all our main commands to the JLine completion engine + for (String cmd : commands.keySet()) { + out.add(cmd, getCompletersForCommand(cmd)); + } + + // Tell JLine to wire up all shortcuts to the exact same completion logic + out.addAliases(aliases); + out.compile(); + return out; + } + + @Override + public Object invoke(CommandSession session, String command, Object... args) throws Exception { + // Resolve shortcut to full command, or keep as-is + String actualCommand = aliases.getOrDefault(command, command); + CommandMethods methods = commands.get(actualCommand); + + if (methods != null && methods.execute() != null) { + CommandInput input = new CommandInput(command, args, session); + return methods.execute().apply(input); + } + return null; + } + + private List getCompletersForCommand(String command) { + String actualCommand = aliases.getOrDefault(command, command); + CommandMethods methods = commands.get(actualCommand); + + if (methods != null && methods.compileCompleter() != null) { + return methods.compileCompleter().apply(actualCommand); + } + return Collections.singletonList(NullCompleter.INSTANCE); + } +} \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java index 2a2992c73b..cf63b870a7 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/AbstractKnoxShellCommand.java @@ -18,13 +18,16 @@ package org.apache.knox.gateway.shell.commands; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.List; import org.apache.groovy.groovysh.jline.GroovyEngine; import org.apache.knox.gateway.shell.CredentialCollectionException; import org.apache.knox.gateway.shell.CredentialCollector; +import org.jline.reader.Completer; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; +import org.jline.reader.impl.completer.NullCompleter; import org.jline.terminal.Terminal; public abstract class AbstractKnoxShellCommand { @@ -69,6 +72,10 @@ public String getHelp() { return help; } + public List getCompleters() { + return Collections.singletonList(NullCompleter.INSTANCE); + } + public abstract Object execute(List args) throws Exception; protected String getBindingVariableNameForResultingTable(List args) { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index 346014488f..6b7ad1276f 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -20,8 +20,11 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.knox.gateway.shell.CredentialCollectionException; import org.apache.knox.gateway.shell.CredentialCollector; @@ -29,6 +32,9 @@ import org.apache.knox.gateway.shell.table.KnoxShellTable; import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.StringsCompleter; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; @@ -165,6 +171,47 @@ private KnoxShellTable buildTable() { return datasource; } + @Override + public List getCompleters() { + + // 1st Argument Completer: Suggests the sub-commands + Completer subCommandCompleter = new StringsCompleter("add", "remove", "select", "list"); + + // 2nd Argument Completer: Suggests Data Source names dynamically + Completer nameCompleter = (reader, parsedLine, candidates) -> { + // parsedLine.words() gives us the exact tokens typed so far (e.g., [":ds", "select", ""]) + List words = parsedLine.words(); + + // Make sure the user has actually typed a sub-command + if (words.size() >= 2) { + String subCommand = words.get(1); // gets "select", "remove", etc. + + // We only want to suggest existing names if they are selecting or removing + if ("select".equalsIgnoreCase(subCommand) || "remove".equalsIgnoreCase(subCommand)) { + List activeDataSources = getDataSourcesNames(); + for (String dsName : activeDataSources) { + candidates.add(new Candidate(dsName)); + } + } + } + }; + + // Return them in positional order: [Arg1, Arg2] + return Arrays.asList(subCommandCompleter, nameCompleter); + } + + private List getDataSourcesNames() { + Map dataSources = getDataSources(); + if (dataSources == null || dataSources.isEmpty()) { + return Collections.emptyList(); + } else { + return dataSources.values() + .stream() + .map(KnoxDataSource::getName) + .collect(Collectors.toList()); + } + } + public static void main(String[] args) { try { Terminal terminal = TerminalBuilder.builder().system(true).build(); From c168b5c4778af43de7e121ab16cd4afb2cd6687c Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:54:27 +0100 Subject: [PATCH 15/40] KNOX-3278: command completion pt2 --- .../org/apache/knox/gateway/shell/Shell.java | 45 +++++++------ .../gateway/shell/SimpleCommandRegistry.java | 2 +- .../shell/commands/DataSourceCommand.java | 63 +++++++++++-------- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 855f949d2f..7a0ff93b48 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -44,7 +44,6 @@ import org.jline.reader.impl.DefaultParser; import org.jline.reader.impl.completer.AggregateCompleter; import org.jline.reader.impl.completer.NullCompleter; -import org.jline.reader.impl.completer.StringsCompleter; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; @@ -123,51 +122,61 @@ private static void startInteractiveShell() throws Exception { Map commandAliases = new HashMap<>(); - registry.forEach((name, cmd) -> { - String shortcut = cmd.getShortcut(); // Use your actual getter method here - if (shortcut != null && !shortcut.isEmpty()) { - commandAliases.put(shortcut, name); // e.g., "ds" -> "datasource" + registry.forEach((rawName, cmd) -> { + + // 1. THE FIX: Force the JLine registry to know the commands start with a colon + String name = rawName.startsWith(":") ? rawName : ":" + rawName; + + String rawShortcut = cmd.getShortcut(); + if (rawShortcut != null && !rawShortcut.isEmpty()) { + String shortcut = rawShortcut.startsWith(":") ? rawShortcut : ":" + rawShortcut; + commandAliases.put(shortcut, name); } + // 2. Put them in the map with the colon-prefixed names commandMethods.put(name, new CommandMethods( - // 1. Execution Logic: CommandInput -> String[] (input) -> { try { - // CommandInput.args() includes the command name as the first element String[] allTokens = input.args(); - - // Convert to List and skip the first element (the command name) + // input.args() includes the command name, so we skip(1) to get the arguments List argsList = (allTokens != null && allTokens.length > 1) ? Arrays.stream(allTokens).skip(1).collect(Collectors.toList()) : Collections.emptyList(); + return cmd.execute(argsList); } catch (Exception e) { - e.printStackTrace(); + input.session().terminal().writer().println("Error: " + e.getMessage()); return null; } }, - // 2. Completion Logic: String (command name) -> Completer - (line) -> cmd.getCompleters() + (line) -> { + List completers = cmd.getCompleters(); + return (completers != null && !completers.isEmpty()) + ? completers + : Collections.singletonList(NullCompleter.INSTANCE); + } )); }); DefaultParser parser = new DefaultParser(); Path workDir = Paths.get(System.getProperty("user.dir")); + SimpleCommandRegistry knoxRegistry = new SimpleCommandRegistry(commandMethods, commandAliases); SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null); - systemRegistry.register("knox", new SimpleCommandRegistry(commandMethods, commandAliases)); - systemRegistry.setCommandRegistries(); + systemRegistry.setCommandRegistries(knoxRegistry); SystemRegistry.add(systemRegistry); // 4. Setup Tab Completers // StringsCompleter automatically suggests our custom commands (e.g., ":sql", ":fs") - Completer knoxCompleter = new StringsCompleter(registry.keySet()); - Completer groovyCompleter = engine.getScriptCompleter(); - Completer finalCompleter = new AggregateCompleter(knoxCompleter, groovyCompleter); + + Completer combinedCompleter = new AggregateCompleter( + systemRegistry.completer(), + engine.getScriptCompleter() + ); // 5. Build the LineReader LineReader reader = LineReaderBuilder.builder() .terminal(terminal) - .completer(finalCompleter) + .completer(combinedCompleter) .variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".knoxshell_history")) .build(); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java index 6eb44cf4ba..8364cc6ce7 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java @@ -72,7 +72,7 @@ public SystemCompleter compileCompleters() { // Tell JLine to wire up all shortcuts to the exact same completion logic out.addAliases(aliases); - out.compile(); + //out.compile(); return out; } diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index 6b7ad1276f..c363f3a95b 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -34,6 +34,8 @@ import org.apache.groovy.groovysh.jline.GroovyEngine; import org.jline.reader.Candidate; import org.jline.reader.Completer; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; import org.jline.reader.impl.completer.StringsCompleter; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; @@ -171,34 +173,45 @@ private KnoxShellTable buildTable() { return datasource; } - @Override - public List getCompleters() { - - // 1st Argument Completer: Suggests the sub-commands - Completer subCommandCompleter = new StringsCompleter("add", "remove", "select", "list"); - - // 2nd Argument Completer: Suggests Data Source names dynamically - Completer nameCompleter = (reader, parsedLine, candidates) -> { - // parsedLine.words() gives us the exact tokens typed so far (e.g., [":ds", "select", ""]) - List words = parsedLine.words(); + @Override + public List getCompleters() { + + // Index 0: The command name itself (e.g., :ds). + // Because Shell.java routes this blindly, we just need a dummy placeholder + // so ArgumentCompleter correctly shifts the subcommands to Index 1. + Completer commandPlaceholder = (reader, parsedLine, candidates) -> { + }; + + // Index 1: Subcommands + Completer subCommandCompleter = new StringsCompleter("add", "remove", "select", "list"); + + // Index 2: Dynamic Data Source Names + Completer nameCompleter = (reader, parsedLine, candidates) -> { + List words = parsedLine.words(); + + // Safety guard against JLine background scans + if (words.size() > 1) { + String subCommand = words.get(1); + if ("select".equalsIgnoreCase(subCommand) || "remove".equalsIgnoreCase(subCommand)) { + List activeDataSources = getDataSourcesNames(); // Your method + for (String dsName : activeDataSources) { + candidates.add(new Candidate(dsName)); + } + } + } + }; - // Make sure the user has actually typed a sub-command - if (words.size() >= 2) { - String subCommand = words.get(1); // gets "select", "remove", etc. + ArgumentCompleter argCompleter = new ArgumentCompleter( + commandPlaceholder, + subCommandCompleter, + nameCompleter, + NullCompleter.INSTANCE // Stops suggesting after the DB name + ); - // We only want to suggest existing names if they are selecting or removing - if ("select".equalsIgnoreCase(subCommand) || "remove".equalsIgnoreCase(subCommand)) { - List activeDataSources = getDataSourcesNames(); - for (String dsName : activeDataSources) { - candidates.add(new Candidate(dsName)); - } - } - } - }; + // Return as a singleton list so Shell.java can just blindly grab it + return Collections.singletonList(argCompleter); + } - // Return them in positional order: [Arg1, Arg2] - return Arrays.asList(subCommandCompleter, nameCompleter); - } private List getDataSourcesNames() { Map dataSources = getDataSources(); From c4a3906b3bb980e245797a836aafab87bedd4a98 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:32:07 +0100 Subject: [PATCH 16/40] KNOX-3278: correct checkstyle errors --- .../org/apache/knox/gateway/shell/SimpleCommandRegistry.java | 5 ++++- .../knox/gateway/shell/commands/DataSourceCommand.java | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java index 8364cc6ce7..85c713f43f 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java @@ -24,7 +24,10 @@ import org.jline.reader.impl.completer.NullCompleter; import org.jline.reader.impl.completer.SystemCompleter; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; public class SimpleCommandRegistry implements CommandRegistry { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index c363f3a95b..2c131e948b 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -20,7 +20,6 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; From ec150f65d4b2ace1b3f5774c17bc67519899ec8a Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:25:35 +0100 Subject: [PATCH 17/40] KNOX-3278: correct undeclared jline module dependencies. --- gateway-shell/pom.xml | 8 ++++++++ pom.xml | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/gateway-shell/pom.xml b/gateway-shell/pom.xml index 42ec57e09f..bb718e8c40 100644 --- a/gateway-shell/pom.xml +++ b/gateway-shell/pom.xml @@ -74,6 +74,14 @@ org.jline jansi + + org.jline + jline-builtins + + + org.jline + jline-console + org.jline jline-reader diff --git a/pom.xml b/pom.xml index 7e9456b552..7512f56aa9 100644 --- a/pom.xml +++ b/pom.xml @@ -1656,6 +1656,16 @@ jansi ${jline.version} + + org.jline + jline-builtins + ${jline.version} + + + org.jline + jline-console + ${jline.version} + org.jline jline-reader From 6a4c6c63c947796c54d0d9e0550a4901fcc9793d Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:56:57 +0100 Subject: [PATCH 18/40] KNOX-3278: cleanup DataSourceCommand completer. --- .../gateway/shell/SimpleCommandRegistry.java | 1 - .../shell/commands/DataSourceCommand.java | 74 ++++++++++--------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java index 85c713f43f..1e67a47f6a 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java @@ -75,7 +75,6 @@ public SystemCompleter compileCompleters() { // Tell JLine to wire up all shortcuts to the exact same completion logic out.addAliases(aliases); - //out.compile(); return out; } diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java index 2c131e948b..8a4fe651c4 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/DataSourceCommand.java @@ -172,44 +172,46 @@ private KnoxShellTable buildTable() { return datasource; } - @Override - public List getCompleters() { - - // Index 0: The command name itself (e.g., :ds). - // Because Shell.java routes this blindly, we just need a dummy placeholder - // so ArgumentCompleter correctly shifts the subcommands to Index 1. - Completer commandPlaceholder = (reader, parsedLine, candidates) -> { - }; - - // Index 1: Subcommands - Completer subCommandCompleter = new StringsCompleter("add", "remove", "select", "list"); - - // Index 2: Dynamic Data Source Names - Completer nameCompleter = (reader, parsedLine, candidates) -> { - List words = parsedLine.words(); - - // Safety guard against JLine background scans - if (words.size() > 1) { - String subCommand = words.get(1); - if ("select".equalsIgnoreCase(subCommand) || "remove".equalsIgnoreCase(subCommand)) { - List activeDataSources = getDataSourcesNames(); // Your method - for (String dsName : activeDataSources) { - candidates.add(new Candidate(dsName)); - } - } - } - }; + @Override + public List getCompleters() { - ArgumentCompleter argCompleter = new ArgumentCompleter( - commandPlaceholder, - subCommandCompleter, - nameCompleter, - NullCompleter.INSTANCE // Stops suggesting after the DB name - ); + // Index 0: The command name itself (e.g., :ds). + // Because Shell.java routes this blindly, we just need a dummy placeholder + // so ArgumentCompleter correctly shifts the subcommands to Index 1. + Completer commandPlaceholder = (reader, parsedLine, candidates) -> {}; - // Return as a singleton list so Shell.java can just blindly grab it - return Collections.singletonList(argCompleter); - } + // Index 1: Subcommands + Completer subCommandCompleter = new StringsCompleter("add", "remove", "select", "list"); + + // Index 2: Dynamic Data Source Names + Completer nameCompleter = dataSourceNameCompleter(); + + ArgumentCompleter argCompleter = new ArgumentCompleter( + commandPlaceholder, + subCommandCompleter, + nameCompleter, + NullCompleter.INSTANCE // Stops suggesting after the DB name + ); + + // Return as a singleton list so Shell.java can just blindly grab it + return Collections.singletonList(argCompleter); + } + + private Completer dataSourceNameCompleter() { + return (reader, parsedLine, candidates) -> { + List words = parsedLine.words(); + // Safety guard against JLine background scans + if (words.size() > 1) { + String subCommand = words.get(1); + if ("select".equalsIgnoreCase(subCommand) || "remove".equalsIgnoreCase(subCommand)) { + List activeDataSources = getDataSourcesNames(); // Your method + for (String dsName : activeDataSources) { + candidates.add(new Candidate(dsName)); + } + } + } + }; + } private List getDataSourcesNames() { From 8e49fd3f330d2fb58a8b57e458acfa93d674af13 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:14:54 +0200 Subject: [PATCH 19/40] KNOX-3278: add :x and :q as exit commands. --- .../main/java/org/apache/knox/gateway/shell/Shell.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 7a0ff93b48..f6a10fa2e9 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -63,6 +63,8 @@ public class Shell { private static final List NON_INTERACTIVE_COMMANDS = Arrays.asList("buildTrustStore", "init", "list", "destroy", "knoxline"); + private static final List EXIT_COMMANDS = Arrays.asList(":exit", ":x", ":quit", ":q"); + private static final String[] IMPORTS = new String[] { KnoxSession.class.getName(), HBase.class.getName(), @@ -181,7 +183,7 @@ private static void startInteractiveShell() throws Exception { .build(); terminal.writer().println("Apache Knox Shell"); - terminal.writer().println("Type ':help' for help, ':exit' or ':quit' to quit."); + terminal.writer().println("Type ':help' for help, ':exit' or ':quit' (':x' or ':q') to quit."); terminal.writer().flush(); // 6. Setup Shutdown Hook (Calling closeConnections directly on our object instances) @@ -206,7 +208,7 @@ private static void startInteractiveShell() throws Exception { } // --- BUILT-IN COMMANDS --- - if (":exit".equalsIgnoreCase(trimmed) || ":quit".equalsIgnoreCase(trimmed)) { + if (EXIT_COMMANDS.stream().anyMatch(trimmed::equalsIgnoreCase)) { break; } @@ -236,7 +238,7 @@ private static void startInteractiveShell() throws Exception { terminal.writer().println(); terminal.writer().printf(Locale.ROOT, " %-25s %s%n", ":help, :h", "Displays this help message or specific command usage"); - terminal.writer().printf(Locale.ROOT, " %-25s %s%n", ":exit, :quit", "Exits the shell"); + terminal.writer().printf(Locale.ROOT, " %-25s %s%n", ":exit, :x, :quit, :q", "Exits the shell"); terminal.writer().println("\nNote: Any other input is evaluated natively as Groovy code."); } terminal.writer().flush(); From 3aafe587c40518cc14937e3969240f5823e0e9c9 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:32:19 +0200 Subject: [PATCH 20/40] KNOX-3278: add '?' as help command alias. --- .../src/main/java/org/apache/knox/gateway/shell/Shell.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index f6a10fa2e9..056a79edfc 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -65,6 +65,8 @@ public class Shell { private static final List EXIT_COMMANDS = Arrays.asList(":exit", ":x", ":quit", ":q"); + private static final List HELP_COMMANDS = Arrays.asList(":help", ":h", "?"); + private static final String[] IMPORTS = new String[] { KnoxSession.class.getName(), HBase.class.getName(), @@ -212,7 +214,7 @@ private static void startInteractiveShell() throws Exception { break; } - if (trimmed.startsWith(":help") || trimmed.startsWith(":h")) { + if (HELP_COMMANDS.stream().anyMatch(h -> trimmed.equalsIgnoreCase(h) || trimmed.startsWith(h + " "))) { String[] helpParts = trimmed.split("\\s+"); if (helpParts.length > 1) { @@ -237,7 +239,7 @@ private static void startInteractiveShell() throws Exception { }); terminal.writer().println(); - terminal.writer().printf(Locale.ROOT, " %-25s %s%n", ":help, :h", "Displays this help message or specific command usage"); + terminal.writer().printf(Locale.ROOT, " %-25s %s%n", ":help, :h, ?", "Displays this help message or specific command usage"); terminal.writer().printf(Locale.ROOT, " %-25s %s%n", ":exit, :x, :quit, :q", "Exits the shell"); terminal.writer().println("\nNote: Any other input is evaluated natively as Groovy code."); } From c5d63ccecef164a4c8ab3dc7f2aac6a70dac7ea1 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:21:29 +0200 Subject: [PATCH 21/40] KNOX-3278: import, load and show commands added. --- .../org/apache/knox/gateway/shell/Shell.java | 10 ++ .../gateway/shell/commands/ImportCommand.java | 110 +++++++++++++ .../gateway/shell/commands/LoadCommand.java | 148 ++++++++++++++++++ .../gateway/shell/commands/ShowCommand.java | 109 +++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java create mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java create mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 056a79edfc..39a209b01e 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -23,7 +23,10 @@ import org.apache.knox.gateway.shell.commands.AbstractKnoxShellCommand; import org.apache.knox.gateway.shell.commands.CSVCommand; import org.apache.knox.gateway.shell.commands.DataSourceCommand; +import org.apache.knox.gateway.shell.commands.ImportCommand; +import org.apache.knox.gateway.shell.commands.LoadCommand; import org.apache.knox.gateway.shell.commands.SelectCommand; +import org.apache.knox.gateway.shell.commands.ShowCommand; import org.apache.knox.gateway.shell.commands.WebHDFSCommand; import org.apache.knox.gateway.shell.hbase.HBase; import org.apache.knox.gateway.shell.hdfs.Hdfs; @@ -106,8 +109,10 @@ private static void startInteractiveShell() throws Exception { GroovyEngine engine = new GroovyEngine(); // 2. Pre-load Knox imports + ImportCommand importCmd = new ImportCommand(engine, terminal); for (String name : IMPORTS) { engine.execute("import " + name); + importCmd.registerBuiltIn(name); } // 3. Instantiate and Map Custom Commands @@ -116,11 +121,16 @@ private static void startInteractiveShell() throws Exception { DataSourceCommand dsCmd = new DataSourceCommand(engine, terminal); CSVCommand csvCmd = new CSVCommand(engine, terminal); WebHDFSCommand hdfsCmd = new WebHDFSCommand(engine, terminal); + ShowCommand showCmd = new ShowCommand(engine, terminal, importCmd); + LoadCommand loadCmd = new LoadCommand(engine, terminal); + registerCommand(registry, importCmd); registerCommand(registry, selectCmd); registerCommand(registry, dsCmd); registerCommand(registry, csvCmd); registerCommand(registry, hdfsCmd); + registerCommand(registry, showCmd); + registerCommand(registry, loadCmd); Map commandMethods = new HashMap<>(); Map commandAliases = new HashMap<>(); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java new file mode 100644 index 0000000000..80189933ee --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.shell.commands; + +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.terminal.Terminal; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Manages Groovy imports in the current shell session. + *

+ * Usage: + * :import - lists active imports + * :import org.apache.knox.gateway.shell.KnoxSession - adds a single import + * :import org.apache.knox.gateway.shell.* - wildcard import + */ +public class ImportCommand extends AbstractKnoxShellCommand { + + private static final String NAME = ":import"; + private static final String SHORTCUT = ":i"; + private static final String DESC = "Import a class into the namespace"; + private static final String USAGE = "Usage: :import []\n" + + " :import - list active imports\n" + + " :import - add a new import\n" + + " :import .* - wildcard import"; + private static final String HELP = USAGE; + + private final Set activeImports = new LinkedHashSet<>(); + + public ImportCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); + } + + /** + * Registers a built-in import so it appears in the :import listing. + * Called from Shell during startup for each pre-loaded Knox import. + */ + public void registerBuiltIn(String fqcn) { + activeImports.add(fqcn); + } + + /** + * Returns an unmodifiable view of the currently active imports. + * Used by ShowCommand to display imports via :show imports. + */ + public Set getActiveImports() { + return Collections.unmodifiableSet(activeImports); + } + + @Override + public Object execute(List args) { + if (args == null || args.isEmpty()) { + // List mode + if (activeImports.isEmpty()) { + terminal.writer().println("No imports registered."); + } else { + terminal.writer().println("Active imports:"); + activeImports.forEach(i -> terminal.writer().println(" import " + i)); + } + terminal.writer().flush(); + return null; + } + + // Join all args in case user typed "import java.util. *" with spaces + String target = String.join("", args).trim(); + + // Strip leading "import " if the user typed ":import import java.util.List" + if (target.toLowerCase().startsWith("import ")) { + target = target.substring(7).trim(); + } + + // Basic validation: must contain a dot (package separator) + if (!target.contains(".")) { + terminal.writer().println("Invalid import: '" + target + + "'. Expected a fully-qualified class or package (e.g. java.util.List or java.util.*)."); + terminal.writer().flush(); + return null; + } + + try { + engine.execute("import " + target); + activeImports.add(target); + terminal.writer().println("==> import " + target); + } catch (Exception e) { + terminal.writer().println("Failed to import '" + target + "': " + e.getMessage()); + } + + terminal.writer().flush(); + return null; + } +} \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java new file mode 100644 index 0000000000..982ed51346 --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.shell.commands; + +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.terminal.Terminal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Loads a Groovy script file or URL into the shell and executes it. + * Matches the old Groovysh :load command behavior. + *

+ * Usage: + * :load /path/to/script.groovy + * :load ~/scripts/setup.groovy + * :load https://example.com/script.groovy + * . /path/to/script.groovy (alias) + */ +public class LoadCommand extends AbstractKnoxShellCommand { + + private static final String NAME = ":load"; + private static final String SHORTCUT = "."; + private static final String DESC = "Load a file or URL into the buffer"; + private static final String USAGE = "Usage: :load \n" + + " :load /path/to/script.groovy\n" + + " :load ~/scripts/setup.groovy\n" + + " :load https://example.com/script.groovy\n" + + " . /path/to/script.groovy"; + private static final String HELP = USAGE; + + public LoadCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); + } + + @Override + public Object execute(List args) throws Exception { + if (args == null || args.isEmpty()) { + terminal.writer().println(USAGE); + terminal.writer().flush(); + return null; + } + + // Join args to support paths with spaces (e.g., :load /my path/script.groovy) + String location = String.join(" ", args).trim(); + + String script; + try { + script = readScript(location); + } catch (Exception e) { + terminal.writer().println("Failed to load '" + location + "': " + e.getMessage()); + terminal.writer().flush(); + return null; + } + + if (script.isEmpty()) { + terminal.writer().println("Warning: '" + location + "' is empty, nothing to execute."); + terminal.writer().flush(); + return null; + } + + terminal.writer().println("Loading " + location + " ..."); + terminal.writer().flush(); + + try { + Object result = engine.execute(script); + if (result != null) { + terminal.writer().println("==> " + result); + terminal.writer().flush(); + } + return result; + } catch (Exception e) { + terminal.writer().println("Error executing script: " + e.getMessage()); + terminal.writer().flush(); + return null; + } + } + + private String readScript(String location) throws IOException { + // Try as URL first (http://, https://, file://) + if (isUrl(location)) { + return readFromUrl(location); + } + + // Expand ~ to user home + if (location.startsWith("~")) { + location = System.getProperty("user.home") + location.substring(1); + } + + Path path = Paths.get(location); + if (!Files.exists(path)) { + throw new IOException("File not found: " + path.toAbsolutePath()); + } + if (!Files.isReadable(path)) { + throw new IOException("File is not readable: " + path.toAbsolutePath()); + } + if (Files.isDirectory(path)) { + throw new IOException("Path is a directory, not a file: " + path.toAbsolutePath()); + } + + return Files.readString(path); + } + + private boolean isUrl(String location) { + return location.startsWith("http://") + || location.startsWith("https://") + || location.startsWith("file://"); + } + + private String readFromUrl(String urlStr) throws IOException { + URL url; + try { + url = new URL(urlStr); + } catch (MalformedURLException e) { + throw new IOException("Invalid URL: " + urlStr, e); + } + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java new file mode 100644 index 0000000000..d673c05e8e --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.shell.commands; + +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.terminal.Terminal; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Shows variables, classes or imports in the current shell session. + *

+ * Usage: + * :show - lists all variables (default) + * :show variables - lists all variables + * :show imports - lists active imports + * :show all - lists both variables and imports + */ +public class ShowCommand extends AbstractKnoxShellCommand { + + private static final String NAME = ":show"; + private static final String SHORTCUT = ":S"; + private static final String DESC = "Show variables, classes or imports"; + private static final String USAGE = "Usage: :show [variables|imports|all]"; + private static final String HELP = USAGE + "\n" + + " variables - list all bound variables (default)\n" + + " imports - list active import statements\n" + + " all - list both variables and imports"; + + private final ImportCommand importCommand; + + + public ShowCommand(GroovyEngine engine, Terminal terminal, ImportCommand importCommand) { + super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); + this.importCommand = importCommand; + } + + @Override + public Object execute(List args) { + String what = (args == null || args.isEmpty()) ? "variables" : args.get(0).toLowerCase(Locale.ROOT); + + switch (what) { + case "variables": + case "vars": + showVariables(); + break; + case "imports": + showImports(); + break; + case "all": + showVariables(); + terminal.writer().println(); + showImports(); + break; + default: + terminal.writer().println(USAGE); + break; + } + + terminal.writer().flush(); + return null; + } + + private void showVariables() { + Map variables = engine.getVariables(); + if (variables == null || variables.isEmpty()) { + terminal.writer().println("No variables defined."); + return; + } + + terminal.writer().println("Variables:"); + variables.forEach((name, value) -> { + String type = (value != null) ? value.getClass().getSimpleName() : "null"; + String display = (value != null) ? value : "null"; + terminal.writer().printf(Locale.ROOT, " %-25s (%s) = %s%n", name, type, display); + }); + } + + private void showImports() { + if (importCommand == null) { + terminal.writer().println("Import tracking not available."); + return; + } + java.util.Set imports = importCommand.getActiveImports(); + if (imports.isEmpty()) { + terminal.writer().println("No imports registered."); + } else { + terminal.writer().println("Imports:"); + imports.forEach(i -> terminal.writer().println(" import " + i)); + } + } +} \ No newline at end of file From 63eeb63ed94c0ee8d18ac240147a800d5309d4cb Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:02:46 +0200 Subject: [PATCH 22/40] KNOX-3278: purge command added. --- .../org/apache/knox/gateway/shell/Shell.java | 3 + .../gateway/shell/commands/ImportCommand.java | 12 ++ .../gateway/shell/commands/PurgeCommand.java | 179 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 39a209b01e..639b04a33e 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -25,6 +25,7 @@ import org.apache.knox.gateway.shell.commands.DataSourceCommand; import org.apache.knox.gateway.shell.commands.ImportCommand; import org.apache.knox.gateway.shell.commands.LoadCommand; +import org.apache.knox.gateway.shell.commands.PurgeCommand; import org.apache.knox.gateway.shell.commands.SelectCommand; import org.apache.knox.gateway.shell.commands.ShowCommand; import org.apache.knox.gateway.shell.commands.WebHDFSCommand; @@ -123,6 +124,7 @@ private static void startInteractiveShell() throws Exception { WebHDFSCommand hdfsCmd = new WebHDFSCommand(engine, terminal); ShowCommand showCmd = new ShowCommand(engine, terminal, importCmd); LoadCommand loadCmd = new LoadCommand(engine, terminal); + PurgeCommand purgeCommand = new PurgeCommand(engine, terminal, importCmd); registerCommand(registry, importCmd); registerCommand(registry, selectCmd); @@ -131,6 +133,7 @@ private static void startInteractiveShell() throws Exception { registerCommand(registry, hdfsCmd); registerCommand(registry, showCmd); registerCommand(registry, loadCmd); + registerCommand(registry, purgeCommand); Map commandMethods = new HashMap<>(); Map commandAliases = new HashMap<>(); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java index 80189933ee..62faef05d7 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java @@ -66,6 +66,18 @@ public Set getActiveImports() { return Collections.unmodifiableSet(activeImports); } + /** + * Removes an import from the tracked set. + * Note: this does not truly "unimport" from the Groovy classloader, + * but it removes it from the tracked listing and from future :show output. + * + * @param fqcn the fully-qualified class or package.* to remove + * @return true if the import was present and removed + */ + public boolean removeImport(String fqcn) { + return activeImports.remove(fqcn); + } + @Override public Object execute(List args) { if (args == null || args.isEmpty()) { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java new file mode 100644 index 0000000000..9787a2b941 --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.shell.commands; + +import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.terminal.Terminal; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Clears variables, imports or both from the current shell session. + * Internal Knox variables (prefixed with __knox) are preserved by default. + *

+ * Usage: + *

    + *
  • :purge - clears user variables (preserves internal Knox state)
  • + *
  • :purge variables - same as above
  • + *
  • :purge imports - clears user-added imports (preserves built-in Knox imports)
  • + *
  • :purge all - clears both variables and user imports
  • + *
+ *

+ */ +public class PurgeCommand extends AbstractKnoxShellCommand { + + private static final String NAME = ":purge"; + private static final String SHORTCUT = ":p"; + private static final String DESC = "Purge variables, classes, imports or preferences"; + private static final String USAGE = "Usage: :purge [variables|imports|all]"; + private static final String HELP = USAGE + "\n" + + " variables - purge user variables, keep internal Knox state (default)\n" + + " imports - purge user-added imports, keep built-in Knox imports\n" + + " all - purge both variables and user imports"; + + /** Prefix used by Knox internal bindings (__knoxdatasource, __knoxsession, etc.) */ + private static final String KNOX_INTERNAL_PREFIX = "__knox"; + + private final ImportCommand importCommand; + private final Set builtInImports; + + /** + * @param engine the GroovyEngine + * @param terminal the JLine terminal + * @param importCommand the ImportCommand instance (for clearing/resetting imports) + */ + public PurgeCommand(GroovyEngine engine, Terminal terminal, ImportCommand importCommand) { + super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); + this.importCommand = importCommand; + // Snapshot the built-in imports at construction time so we know which ones to preserve + this.builtInImports = Set.copyOf(importCommand.getActiveImports()); + } + + @Override + public Object execute(List args) { + String what = (args == null || args.isEmpty()) ? "variables" : args.get(0).toLowerCase(Locale.ROOT); + + switch (what) { + case "variables": + case "vars": + int varCount = clearVariables(); + terminal.writer().println("Purged " + varCount + " variable(s). Internal Knox state preserved."); + break; + case "imports": + int importCount = clearUserImports(); + terminal.writer().println("Purged " + importCount + " user import(s). Built-in Knox imports preserved."); + break; + case "all": + int vc = clearVariables(); + int ic = clearUserImports(); + terminal.writer().println("Purged " + vc + " variable(s) and " + ic + " user import(s)."); + break; + default: + terminal.writer().println(USAGE); + break; + } + + terminal.writer().flush(); + return null; + } + + private int clearVariables() { + Map variables = engine.getVariables(); + if (variables == null || variables.isEmpty()) { + return 0; + } + + int count = 0; + Iterator> it = variables.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + // Preserve internal Knox bindings + if (!entry.getKey().startsWith(KNOX_INTERNAL_PREFIX)) { + it.remove(); + count++; + } + } + return count; + } + + private int clearUserImports() { + if (importCommand == null) { + return 0; + } + + Set current = importCommand.getActiveImports(); + // Collect user-added imports (those not in the built-in snapshot) + List toRemove = current.stream() + .filter(imp -> !builtInImports.contains(imp)) + .toList(); + + // Remove from ImportCommand's tracked set + toRemove.forEach(importCommand::removeImport); + + return toRemove.size(); + } + + @Override + public List getCompleters() { + // Index 0: command name placeholder (Shell.java handles routing) + Completer commandPlaceholder = (reader, parsedLine, candidates) -> {}; + + // Index 1: subcommands + Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all"); + + // Index 2: dynamic target names (variable names for "variables", import names for "imports") + Completer targetCompleter = (reader, parsedLine, candidates) -> { + List words = parsedLine.words(); + if (words.size() > 1) { + String subCommand = words.get(1).toLowerCase(Locale.ROOT); + if ("variables".equals(subCommand) || "vars".equals(subCommand)) { + // Suggest purgeable variable names (exclude internal __knox* ones) + Map variables = engine.getVariables(); + if (variables != null) { + variables.keySet().stream() + .filter(name -> !name.startsWith(KNOX_INTERNAL_PREFIX)) + .forEach(name -> candidates.add(new Candidate(name))); + } + } else if ("imports".equals(subCommand)) { + // Suggest user-added imports (exclude built-in Knox imports) + importCommand.getActiveImports().stream() + .filter(imp -> !builtInImports.contains(imp)) + .forEach(imp -> candidates.add(new Candidate(imp))); + } + } + }; + + ArgumentCompleter argCompleter = new ArgumentCompleter( + commandPlaceholder, + subCommandCompleter, + targetCompleter, + NullCompleter.INSTANCE); + + return Collections.singletonList(argCompleter); + } +} \ No newline at end of file From 0ac946d0ef061d67d73ac33ee1d7cdd12ff0038b Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:35:47 +0200 Subject: [PATCH 23/40] KNOX-3278: Correcting load command and alias. --- .../org/apache/knox/gateway/shell/Shell.java | 24 ++++++++++++------- .../gateway/shell/commands/LoadCommand.java | 23 ++++++++++++++++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 639b04a33e..5ebfa8c8b9 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -140,15 +140,17 @@ private static void startInteractiveShell() throws Exception { registry.forEach((rawName, cmd) -> { + if (rawName.equals(cmd.getShortcut())) { + return; + } + String name = rawName.startsWith(":") ? rawName : ":" + rawName; - // 1. THE FIX: Force the JLine registry to know the commands start with a colon - String name = rawName.startsWith(":") ? rawName : ":" + rawName; - - String rawShortcut = cmd.getShortcut(); - if (rawShortcut != null && !rawShortcut.isEmpty()) { - String shortcut = rawShortcut.startsWith(":") ? rawShortcut : ":" + rawShortcut; - commandAliases.put(shortcut, name); - } + String rawShortcut = cmd.getShortcut(); + String shortcut = null; + if (rawShortcut != null && !rawShortcut.isEmpty()) { + shortcut = rawShortcut.equals(".") || rawShortcut.startsWith(":") ? rawShortcut : ":" + rawShortcut; + commandAliases.put(shortcut, name); + } // 2. Put them in the map with the colon-prefixed names commandMethods.put(name, new CommandMethods( @@ -176,6 +178,9 @@ private static void startInteractiveShell() throws Exception { }); DefaultParser parser = new DefaultParser(); + // Override default regex to allow '.' as a valid command string + // Original: "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*" + parser.setRegexCommand("(?:\\.|[:]?[a-zA-Z]+[a-zA-Z0-9_-]*)"); Path workDir = Paths.get(System.getProperty("user.dir")); SimpleCommandRegistry knoxRegistry = new SimpleCommandRegistry(commandMethods, commandAliases); SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null); @@ -192,13 +197,14 @@ private static void startInteractiveShell() throws Exception { // 5. Build the LineReader LineReader reader = LineReaderBuilder.builder() + .parser(parser) .terminal(terminal) .completer(combinedCompleter) .variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".knoxshell_history")) .build(); terminal.writer().println("Apache Knox Shell"); - terminal.writer().println("Type ':help' for help, ':exit' or ':quit' (':x' or ':q') to quit."); + terminal.writer().println("Type ':help' (':h' or '?') for help, ':exit' or ':quit' (':x' or ':q') to quit."); terminal.writer().flush(); // 6. Setup Shutdown Hook (Calling closeConnections directly on our object instances) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java index 982ed51346..43652f4ea8 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java @@ -18,6 +18,9 @@ package org.apache.knox.gateway.shell.commands; import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.builtins.Completers; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.NullCompleter; import org.jline.terminal.Terminal; import java.io.BufferedReader; @@ -29,6 +32,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -127,9 +131,10 @@ private String readScript(String location) throws IOException { } private boolean isUrl(String location) { - return location.startsWith("http://") + return location!=null && + (location.startsWith("http://") || location.startsWith("https://") - || location.startsWith("file://"); + || location.startsWith("file://")); } private String readFromUrl(String urlStr) throws IOException { @@ -145,4 +150,18 @@ private String readFromUrl(String urlStr) throws IOException { return reader.lines().collect(Collectors.joining("\n")); } } + + @Override + public List getCompleters() { + Completers.FileNameCompleter fileNameCompleter = new Completers.FileNameCompleter(); + Completer fileCompleter = (reader, parsedLine, candidates) -> { + String word = parsedLine.word(); + if (isUrl(word)) { + return; + } + fileNameCompleter.complete(reader, parsedLine, candidates); + }; + return Arrays.asList(fileCompleter, NullCompleter.INSTANCE); + } + } From ced80966ae5522e87126b64bd27380be9910c790 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:42:13 +0200 Subject: [PATCH 24/40] KNOX-3278: Adding completer for ShowCommand. --- .../knox/gateway/shell/commands/ShowCommand.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java index d673c05e8e..0952e4478f 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java @@ -18,8 +18,12 @@ package org.apache.knox.gateway.shell.commands; import org.apache.groovy.groovysh.jline.GroovyEngine; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.StringsCompleter; import org.jline.terminal.Terminal; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; @@ -37,8 +41,8 @@ public class ShowCommand extends AbstractKnoxShellCommand { private static final String NAME = ":show"; private static final String SHORTCUT = ":S"; - private static final String DESC = "Show variables, classes or imports"; - private static final String USAGE = "Usage: :show [variables|imports|all]"; + private static final String DESC = "Show variables, imports or both"; + private static final String USAGE = "Usage: :show [variables|vars|imports|all]"; private static final String HELP = USAGE + "\n" + " variables - list all bound variables (default)\n" + " imports - list active import statements\n" @@ -106,4 +110,11 @@ private void showImports() { imports.forEach(i -> terminal.writer().println(" import " + i)); } } + + @Override + public List getCompleters() { + Completer subCommandCompleter = new StringsCompleter("variables", "vars", "imports", "all"); + return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE); + } + } \ No newline at end of file From 783aee2e362622561a06e468793a86a751887a69 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:45:11 +0200 Subject: [PATCH 25/40] KNOX-3278: Correct checkstyle error on shortcut handling --- .../src/main/java/org/apache/knox/gateway/shell/Shell.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 5ebfa8c8b9..7f0979a258 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -148,7 +148,7 @@ private static void startInteractiveShell() throws Exception { String rawShortcut = cmd.getShortcut(); String shortcut = null; if (rawShortcut != null && !rawShortcut.isEmpty()) { - shortcut = rawShortcut.equals(".") || rawShortcut.startsWith(":") ? rawShortcut : ":" + rawShortcut; + shortcut = (".".equals(rawShortcut) || ":".equals(rawShortcut)) ? rawShortcut : ":" + rawShortcut; commandAliases.put(shortcut, name); } From 531ccb0a5aeede6e984cf38ff281a96054cb25ff Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:40:16 +0200 Subject: [PATCH 26/40] KNOX-3278: Renaming SimpleCommandRegistry to KnoxShellCommandRegistry and cleaning up command handling. Overriding name() so that tab completion after semicolon does not show the name of the command registry. --- ...stry.java => KnoxShellCommandRegistry.java} | 9 +++++++-- .../org/apache/knox/gateway/shell/Shell.java | 18 +++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) rename gateway-shell/src/main/java/org/apache/knox/gateway/shell/{SimpleCommandRegistry.java => KnoxShellCommandRegistry.java} (93%) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java similarity index 93% rename from gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java rename to gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java index 1e67a47f6a..a401b67172 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SimpleCommandRegistry.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java @@ -29,16 +29,21 @@ import java.util.Map; import java.util.Set; -public class SimpleCommandRegistry implements CommandRegistry { +public class KnoxShellCommandRegistry implements CommandRegistry { private final Map commands; private final Map aliases; - public SimpleCommandRegistry(Map commands, Map aliases) { + public KnoxShellCommandRegistry(Map commands, Map aliases) { this.commands = commands; this.aliases = aliases != null ? aliases : Collections.emptyMap(); } + @Override + public String name() { + return ""; + } + @Override public boolean hasCommand(String command) { return commands.containsKey(command) || aliases.containsKey(command); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 7f0979a258..1b9488f9f4 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -139,16 +139,12 @@ private static void startInteractiveShell() throws Exception { Map commandAliases = new HashMap<>(); - registry.forEach((rawName, cmd) -> { - if (rawName.equals(cmd.getShortcut())) { + registry.forEach((name, cmd) -> { + if (name.equals(cmd.getShortcut())) { return; } - String name = rawName.startsWith(":") ? rawName : ":" + rawName; - - String rawShortcut = cmd.getShortcut(); - String shortcut = null; - if (rawShortcut != null && !rawShortcut.isEmpty()) { - shortcut = (".".equals(rawShortcut) || ":".equals(rawShortcut)) ? rawShortcut : ":" + rawShortcut; + String shortcut = cmd.getShortcut(); + if (shortcut != null && !shortcut.isEmpty()) { commandAliases.put(shortcut, name); } @@ -182,9 +178,9 @@ private static void startInteractiveShell() throws Exception { // Original: "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*" parser.setRegexCommand("(?:\\.|[:]?[a-zA-Z]+[a-zA-Z0-9_-]*)"); Path workDir = Paths.get(System.getProperty("user.dir")); - SimpleCommandRegistry knoxRegistry = new SimpleCommandRegistry(commandMethods, commandAliases); + KnoxShellCommandRegistry knoxShellCommandRegistry = new KnoxShellCommandRegistry(commandMethods, commandAliases); SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null); - systemRegistry.setCommandRegistries(knoxRegistry); + systemRegistry.setCommandRegistries(knoxShellCommandRegistry); SystemRegistry.add(systemRegistry); // 4. Setup Tab Completers @@ -281,7 +277,7 @@ private static void startInteractiveShell() throws Exception { Object res = cmd.execute(cmdArgs); if (res != null) { - terminal.writer().println(res.toString()); + terminal.writer().println(res); } } else { // Fallback to evaluating standard Groovy script logic From b06371bc595914991a9e0fcd39a11ae2d986b499 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:57:38 +0200 Subject: [PATCH 27/40] KNOX-3278: No need to exclude Log4j2Plugins.dat from the shaded knoxhsell jar. --- gateway-shell-release/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml index 3c44f90a1d..31d47caef3 100644 --- a/gateway-shell-release/pom.xml +++ b/gateway-shell-release/pom.xml @@ -58,7 +58,6 @@ schema/** **/*.ldif - META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat From b08b20e4811e37089f1314d597558b79741b3788 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:58:12 +0200 Subject: [PATCH 28/40] KNOX-3278: correcting knoxshell.sh to use -jar instead of main class (to use Launcher taken from knoxshell.jar manifest main class) --- gateway-shell-release/home/bin/knoxshell.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-shell-release/home/bin/knoxshell.sh b/gateway-shell-release/home/bin/knoxshell.sh index 14cea81e79..877441502c 100755 --- a/gateway-shell-release/home/bin/knoxshell.sh +++ b/gateway-shell-release/home/bin/knoxshell.sh @@ -81,7 +81,7 @@ function main { checkJava buildAppJavaOpts - $JAVA "${APP_JAVA_OPTS[@]}" -Dlog4j.configurationFile=conf/knoxshell-log4j2.xml -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -cp "$APP_JAR":lib/* org.apache.knox.gateway.shell.Shell "$@" || exit 1 + $JAVA "${APP_JAVA_OPTS[@]}" -Dlog4j.configurationFile=conf/knoxshell-log4j2.xml -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -cp "$APP_JAR":lib/* -jar "$APP_JAR" "$@" || exit 1 return 0 } From ca30d3684cb7c6ad86952d287a88779afc7b8bb2 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:34:35 +0200 Subject: [PATCH 29/40] KNOX-3278: excluding Log4j2Plugins.dat from the shaded knoxhsell jar and making knoxshell-log4j2.xml consistent with other log4j2 configurations. --- gateway-shell-release/home/conf/knoxshell-log4j2.xml | 2 +- gateway-shell-release/pom.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gateway-shell-release/home/conf/knoxshell-log4j2.xml b/gateway-shell-release/home/conf/knoxshell-log4j2.xml index b8997b0302..275094a344 100644 --- a/gateway-shell-release/home/conf/knoxshell-log4j2.xml +++ b/gateway-shell-release/home/conf/knoxshell-log4j2.xml @@ -17,7 +17,7 @@ --> - logs + ${sys:launcher.dir}/../logs ${sys:launcher.name}.log diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml index 31d47caef3..3c44f90a1d 100644 --- a/gateway-shell-release/pom.xml +++ b/gateway-shell-release/pom.xml @@ -58,6 +58,7 @@ schema/** **/*.ldif + META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat From 5556484cff5950e8a9433e62c55943da7432666e Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:40:04 +0200 Subject: [PATCH 30/40] KNOX-3278: get rid of useless warning javax.* types are not being woven --- gateway-shell/src/main/resources/META-INF/aop.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-shell/src/main/resources/META-INF/aop.xml b/gateway-shell/src/main/resources/META-INF/aop.xml index 0070403377..7f4fa2514a 100644 --- a/gateway-shell/src/main/resources/META-INF/aop.xml +++ b/gateway-shell/src/main/resources/META-INF/aop.xml @@ -20,7 +20,7 @@ - + From ddc891a30b6816b4d49bc58661f64f94f97c9050 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:16:49 +0200 Subject: [PATCH 31/40] KNOX-3278: fix log4j2 WARNING: sun.reflect.Reflection.getCallerClass is not supported. This will impact performance. --- gateway-shell-release/pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml index 3c44f90a1d..0724c460a0 100644 --- a/gateway-shell-release/pom.xml +++ b/gateway-shell-release/pom.xml @@ -49,6 +49,9 @@ org.apache.knox.gateway.launcher.Launcher + + true + From 294fc5be4a797d48e15c1acd7ad41fade23b9e51 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:21:54 +0200 Subject: [PATCH 32/40] KNOX-3278: Fix purge command, simplify import and show commands. --- .../org/apache/knox/gateway/shell/Shell.java | 12 +-- .../gateway/shell/commands/ImportCommand.java | 43 ++------ .../gateway/shell/commands/PurgeCommand.java | 97 +++++-------------- .../gateway/shell/commands/ShowCommand.java | 21 ++-- 4 files changed, 44 insertions(+), 129 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 1b9488f9f4..029ed355e2 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -110,21 +110,21 @@ private static void startInteractiveShell() throws Exception { GroovyEngine engine = new GroovyEngine(); // 2. Pre-load Knox imports - ImportCommand importCmd = new ImportCommand(engine, terminal); for (String name : IMPORTS) { engine.execute("import " + name); - importCmd.registerBuiltIn(name); } // 3. Instantiate and Map Custom Commands Map registry = new HashMap<>(); - SelectCommand selectCmd = new SelectCommand(engine, terminal); - DataSourceCommand dsCmd = new DataSourceCommand(engine, terminal); CSVCommand csvCmd = new CSVCommand(engine, terminal); + DataSourceCommand dsCmd = new DataSourceCommand(engine, terminal); + SelectCommand selectCmd = new SelectCommand(engine, terminal); WebHDFSCommand hdfsCmd = new WebHDFSCommand(engine, terminal); - ShowCommand showCmd = new ShowCommand(engine, terminal, importCmd); + + ImportCommand importCmd = new ImportCommand(engine, terminal); LoadCommand loadCmd = new LoadCommand(engine, terminal); - PurgeCommand purgeCommand = new PurgeCommand(engine, terminal, importCmd); + PurgeCommand purgeCommand = new PurgeCommand(engine, terminal); + ShowCommand showCmd = new ShowCommand(engine, terminal); registerCommand(registry, importCmd); registerCommand(registry, selectCmd); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java index 62faef05d7..e607788195 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java @@ -20,10 +20,8 @@ import org.apache.groovy.groovysh.jline.GroovyEngine; import org.jline.terminal.Terminal; -import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; +import java.util.Map; /** * Manages Groovy imports in the current shell session. @@ -44,50 +42,22 @@ public class ImportCommand extends AbstractKnoxShellCommand { + " :import .* - wildcard import"; private static final String HELP = USAGE; - private final Set activeImports = new LinkedHashSet<>(); - public ImportCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); } - /** - * Registers a built-in import so it appears in the :import listing. - * Called from Shell during startup for each pre-loaded Knox import. - */ - public void registerBuiltIn(String fqcn) { - activeImports.add(fqcn); - } - - /** - * Returns an unmodifiable view of the currently active imports. - * Used by ShowCommand to display imports via :show imports. - */ - public Set getActiveImports() { - return Collections.unmodifiableSet(activeImports); - } - - /** - * Removes an import from the tracked set. - * Note: this does not truly "unimport" from the Groovy classloader, - * but it removes it from the tracked listing and from future :show output. - * - * @param fqcn the fully-qualified class or package.* to remove - * @return true if the import was present and removed - */ - public boolean removeImport(String fqcn) { - return activeImports.remove(fqcn); - } - @Override public Object execute(List args) { if (args == null || args.isEmpty()) { // List mode - if (activeImports.isEmpty()) { + Map imports = engine.getImports(); + if (imports.isEmpty()) { terminal.writer().println("No imports registered."); } else { terminal.writer().println("Active imports:"); - activeImports.forEach(i -> terminal.writer().println(" import " + i)); - } + imports.keySet().stream() + .sorted() + .forEach(key -> terminal.writer().println(" import " + key)); } terminal.writer().flush(); return null; } @@ -110,7 +80,6 @@ public Object execute(List args) { try { engine.execute("import " + target); - activeImports.add(target); terminal.writer().println("==> import " + target); } catch (Exception e) { terminal.writer().println("Failed to import '" + target + "': " + e.getMessage()); diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java index 9787a2b941..92122bc475 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java @@ -18,19 +18,15 @@ package org.apache.knox.gateway.shell.commands; import org.apache.groovy.groovysh.jline.GroovyEngine; -import org.jline.reader.Candidate; import org.jline.reader.Completer; -import org.jline.reader.impl.completer.ArgumentCompleter; import org.jline.reader.impl.completer.NullCompleter; import org.jline.reader.impl.completer.StringsCompleter; import org.jline.terminal.Terminal; -import java.util.Collections; -import java.util.Iterator; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; /** * Clears variables, imports or both from the current shell session. @@ -52,26 +48,19 @@ public class PurgeCommand extends AbstractKnoxShellCommand { private static final String DESC = "Purge variables, classes, imports or preferences"; private static final String USAGE = "Usage: :purge [variables|imports|all]"; private static final String HELP = USAGE + "\n" - + " variables - purge user variables, keep internal Knox state (default)\n" + + " variables - purge user variables, keep internal Knox state\n" + " imports - purge user-added imports, keep built-in Knox imports\n" + " all - purge both variables and user imports"; /** Prefix used by Knox internal bindings (__knoxdatasource, __knoxsession, etc.) */ private static final String KNOX_INTERNAL_PREFIX = "__knox"; - private final ImportCommand importCommand; - private final Set builtInImports; - /** * @param engine the GroovyEngine * @param terminal the JLine terminal - * @param importCommand the ImportCommand instance (for clearing/resetting imports) */ - public PurgeCommand(GroovyEngine engine, Terminal terminal, ImportCommand importCommand) { + public PurgeCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); - this.importCommand = importCommand; - // Snapshot the built-in imports at construction time so we know which ones to preserve - this.builtInImports = Set.copyOf(importCommand.getActiveImports()); } @Override @@ -80,18 +69,17 @@ public Object execute(List args) { switch (what) { case "variables": - case "vars": int varCount = clearVariables(); terminal.writer().println("Purged " + varCount + " variable(s). Internal Knox state preserved."); break; case "imports": - int importCount = clearUserImports(); - terminal.writer().println("Purged " + importCount + " user import(s). Built-in Knox imports preserved."); + int importCount = clearImports(); + terminal.writer().println("Purged " + importCount + " import(s)."); break; case "all": int vc = clearVariables(); - int ic = clearUserImports(); - terminal.writer().println("Purged " + vc + " variable(s) and " + ic + " user import(s)."); + int ic = clearImports(); + terminal.writer().println("Purged " + vc + " variable(s) and " + ic + " import(s)."); break; default: terminal.writer().println(USAGE); @@ -103,77 +91,44 @@ public Object execute(List args) { } private int clearVariables() { - Map variables = engine.getVariables(); + java.util.Map variables = engine.find(); if (variables == null || variables.isEmpty()) { return 0; } int count = 0; - Iterator> it = variables.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); + List keysToDelete = new java.util.ArrayList<>(); + for (String variableName : variables.keySet()) { // Preserve internal Knox bindings - if (!entry.getKey().startsWith(KNOX_INTERNAL_PREFIX)) { - it.remove(); + if (variableName != null && !variableName.startsWith(KNOX_INTERNAL_PREFIX)) { + keysToDelete.add(variableName); count++; } } + if (!keysToDelete.isEmpty()) { + engine.del(keysToDelete.toArray(new String[0])); + } return count; } - private int clearUserImports() { - if (importCommand == null) { + private int clearImports() { + Map imports = engine.getImports(); + + if (imports == null || imports.isEmpty()) { return 0; } - Set current = importCommand.getActiveImports(); - // Collect user-added imports (those not in the built-in snapshot) - List toRemove = current.stream() - .filter(imp -> !builtInImports.contains(imp)) - .toList(); - - // Remove from ImportCommand's tracked set - toRemove.forEach(importCommand::removeImport); - - return toRemove.size(); + int count = 0; + for (String importName : imports.keySet()) { + engine.removeImport(importName); + count++; + } + return count; } @Override public List getCompleters() { - // Index 0: command name placeholder (Shell.java handles routing) - Completer commandPlaceholder = (reader, parsedLine, candidates) -> {}; - - // Index 1: subcommands Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all"); - - // Index 2: dynamic target names (variable names for "variables", import names for "imports") - Completer targetCompleter = (reader, parsedLine, candidates) -> { - List words = parsedLine.words(); - if (words.size() > 1) { - String subCommand = words.get(1).toLowerCase(Locale.ROOT); - if ("variables".equals(subCommand) || "vars".equals(subCommand)) { - // Suggest purgeable variable names (exclude internal __knox* ones) - Map variables = engine.getVariables(); - if (variables != null) { - variables.keySet().stream() - .filter(name -> !name.startsWith(KNOX_INTERNAL_PREFIX)) - .forEach(name -> candidates.add(new Candidate(name))); - } - } else if ("imports".equals(subCommand)) { - // Suggest user-added imports (exclude built-in Knox imports) - importCommand.getActiveImports().stream() - .filter(imp -> !builtInImports.contains(imp)) - .forEach(imp -> candidates.add(new Candidate(imp))); - } - } - }; - - ArgumentCompleter argCompleter = new ArgumentCompleter( - commandPlaceholder, - subCommandCompleter, - targetCompleter, - NullCompleter.INSTANCE); - - return Collections.singletonList(argCompleter); + return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE); } } \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java index 0952e4478f..aba386fba8 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java @@ -42,18 +42,14 @@ public class ShowCommand extends AbstractKnoxShellCommand { private static final String NAME = ":show"; private static final String SHORTCUT = ":S"; private static final String DESC = "Show variables, imports or both"; - private static final String USAGE = "Usage: :show [variables|vars|imports|all]"; + private static final String USAGE = "Usage: :show [variables|imports|all]"; private static final String HELP = USAGE + "\n" + " variables - list all bound variables (default)\n" + " imports - list active import statements\n" + " all - list both variables and imports"; - private final ImportCommand importCommand; - - - public ShowCommand(GroovyEngine engine, Terminal terminal, ImportCommand importCommand) { + public ShowCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); - this.importCommand = importCommand; } @Override @@ -62,7 +58,6 @@ public Object execute(List args) { switch (what) { case "variables": - case "vars": showVariables(); break; case "imports": @@ -83,7 +78,7 @@ public Object execute(List args) { } private void showVariables() { - Map variables = engine.getVariables(); + Map variables = engine.find(); if (variables == null || variables.isEmpty()) { terminal.writer().println("No variables defined."); return; @@ -92,17 +87,13 @@ private void showVariables() { terminal.writer().println("Variables:"); variables.forEach((name, value) -> { String type = (value != null) ? value.getClass().getSimpleName() : "null"; - String display = (value != null) ? value : "null"; + String display = (value != null) ? value.toString() : "null"; terminal.writer().printf(Locale.ROOT, " %-25s (%s) = %s%n", name, type, display); }); } private void showImports() { - if (importCommand == null) { - terminal.writer().println("Import tracking not available."); - return; - } - java.util.Set imports = importCommand.getActiveImports(); + java.util.Set imports = engine.getImports().keySet(); if (imports.isEmpty()) { terminal.writer().println("No imports registered."); } else { @@ -113,7 +104,7 @@ private void showImports() { @Override public List getCompleters() { - Completer subCommandCompleter = new StringsCompleter("variables", "vars", "imports", "all"); + Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all"); return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE); } From 206050b98e2ea62b35a1a4d813e7fc0cab977037 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:32:01 +0200 Subject: [PATCH 33/40] KNOX-3278: Fix ImportCommand and add support for static imports. --- .../gateway/shell/commands/ImportCommand.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java index e607788195..ecee87b95d 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import java.util.regex.Pattern; /** * Manages Groovy imports in the current shell session. @@ -39,9 +40,14 @@ public class ImportCommand extends AbstractKnoxShellCommand { private static final String USAGE = "Usage: :import []\n" + " :import - list active imports\n" + " :import - add a new import\n" - + " :import .* - wildcard import"; + + " :import static - add a static import\n" + + " :import .* - wildcard import"; private static final String HELP = USAGE; + // Groovysh 4.x validation: chars, digits, underscore, dot, star, optional semicolon + private static final Pattern IMPORTED_ITEM_PATTERN = Pattern.compile("^[a-zA-Z0-9_. *]+;?$"); + + public ImportCommand(GroovyEngine engine, Terminal terminal) { super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); } @@ -55,29 +61,25 @@ public Object execute(List args) { terminal.writer().println("No imports registered."); } else { terminal.writer().println("Active imports:"); - imports.keySet().stream() + imports.values().stream() .sorted() - .forEach(key -> terminal.writer().println(" import " + key)); } + .forEach(value -> terminal.writer().println(value)); } terminal.writer().flush(); return null; } - // Join all args in case user typed "import java.util. *" with spaces - String target = String.join("", args).trim(); + // Join with spaces to preserve "static" keyword + String target = String.join(" ", args).trim(); - // Strip leading "import " if the user typed ":import import java.util.List" - if (target.toLowerCase().startsWith("import ")) { - target = target.substring(7).trim(); - } - - // Basic validation: must contain a dot (package separator) - if (!target.contains(".")) { - terminal.writer().println("Invalid import: '" + target - + "'. Expected a fully-qualified class or package (e.g. java.util.List or java.util.*)."); + if (!IMPORTED_ITEM_PATTERN.matcher(target).matches()) { + terminal.writer().println("Invalid import definition: '" + target + "'"); terminal.writer().flush(); return null; } + // Strip Java-style semicolons + target = target.replace(";", ""); + try { engine.execute("import " + target); terminal.writer().println("==> import " + target); From 5dfe353abc9ebfe0fe33496e99b9eefb9ad1b117 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:42:26 +0200 Subject: [PATCH 34/40] KNOX-3278: Catch Throwable similarly to legacy GroovySh. --- .../src/main/java/org/apache/knox/gateway/shell/Shell.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 029ed355e2..7d83c41b8a 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -292,7 +292,8 @@ private static void startInteractiveShell() throws Exception { } catch (UserInterruptException | EndOfFileException e) { // Ctrl+C or Ctrl+D cleanly exits the shell break; - } catch (Exception e) { + } catch (Throwable e) { + // Shell should not exit (similar to legacy GroovySh) terminal.writer().println("Error: " + e.getMessage()); terminal.writer().flush(); } From cf3fa40fb0302d7278f1a006ff62318d5330a08b Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:01:50 +0200 Subject: [PATCH 35/40] KNOX-3278: Make LoadCommand handle multi-file loading as in legacy GroovySh 4. --- .../gateway/shell/commands/LoadCommand.java | 94 +++++++++++++------ 1 file changed, 65 insertions(+), 29 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java index 43652f4ea8..f621d32852 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java @@ -34,7 +34,6 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; /** * Loads a Groovy script file or URL into the shell and executes it. @@ -70,39 +69,52 @@ public Object execute(List args) throws Exception { return null; } - // Join args to support paths with spaces (e.g., :load /my path/script.groovy) - String location = String.join(" ", args).trim(); + Object lastResult = null; - String script; - try { - script = readScript(location); - } catch (Exception e) { - terminal.writer().println("Failed to load '" + location + "': " + e.getMessage()); - terminal.writer().flush(); - return null; - } - - if (script.isEmpty()) { - terminal.writer().println("Warning: '" + location + "' is empty, nothing to execute."); - terminal.writer().flush(); - return null; - } + // Iterate over arguments to support multi-file loading + // (e.g., :load file1.groovy file2.groovy) + for (String location : args) { + String script; + try { + script = readScript(location); + } catch (Exception e) { + terminal.writer().println("Failed to load '" + location + "': " + e.getMessage()); + terminal.writer().flush(); + continue; // Skip to the next file instead of aborting the whole command + } - terminal.writer().println("Loading " + location + " ..."); - terminal.writer().flush(); + // Legacy feature: strip Unix shebangs (#!/usr/bin/env groovy) + if (script.startsWith("#!")) { + int newlineIndex = script.indexOf('\n'); + if (newlineIndex != -1) { + script = script.substring(newlineIndex + 1); + } else { + script = ""; + } + } - try { - Object result = engine.execute(script); - if (result != null) { - terminal.writer().println("==> " + result); + if (script.trim().isEmpty()) { + terminal.writer().println("Warning: '" + location + "' is empty, nothing to execute."); terminal.writer().flush(); + continue; } - return result; - } catch (Exception e) { - terminal.writer().println("Error executing script: " + e.getMessage()); + + terminal.writer().println("Loading " + location + " ..."); terminal.writer().flush(); - return null; + + try { + lastResult = engine.execute(script); + if (lastResult != null) { + terminal.writer().println("==> " + lastResult); + terminal.writer().flush(); + } + } catch (Exception e) { + terminal.writer().println("Error executing script '" + location + "': " + e.getMessage()); + terminal.writer().flush(); + } } + + return lastResult; } private String readScript(String location) throws IOException { @@ -127,7 +139,9 @@ private String readScript(String location) throws IOException { throw new IOException("Path is a directory, not a file: " + path.toAbsolutePath()); } - return Files.readString(path); + try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + return readAndSkipShebang(reader); + } } private boolean isUrl(String location) { @@ -147,8 +161,30 @@ private String readFromUrl(String urlStr) throws IOException { try (BufferedReader reader = new BufferedReader( new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) { - return reader.lines().collect(Collectors.joining("\n")); + return readAndSkipShebang(reader); + } + } + + private String readAndSkipShebang(BufferedReader reader) throws IOException { + String firstLine = reader.readLine(); + if (firstLine == null) { + return ""; + } + + StringBuilder scriptBuilder = new StringBuilder(); + + // If it's not a shebang, preserve the first line + if (!firstLine.startsWith("#!")) { + scriptBuilder.append(firstLine).append(System.lineSeparator()); } + + // Read the rest of the file + String line; + while ((line = reader.readLine()) != null) { + scriptBuilder.append(line).append(System.lineSeparator()); + } + + return scriptBuilder.toString(); } @Override From eeb8d4ba71da0695c7d17d917335e2c1276539d6 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:10:07 +0200 Subject: [PATCH 36/40] KNOX-3278: wrap GroovyEngine completer into SafeCompleter to handle unexpected exceptions. --- .../knox/gateway/shell/SafeCompleter.java | 49 +++++++++++++++++++ .../org/apache/knox/gateway/shell/Shell.java | 3 +- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java new file mode 100644 index 0000000000..fb59eda3ad --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.knox.gateway.shell; + +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +import java.util.List; + +/** + * A wrapper to protect JLine from crashing when underlying completers + * (like Groovy's reflection completer) throw unexpected JVM exceptions. + */ +public class SafeCompleter implements Completer { + private final Completer delegate; + + public SafeCompleter(Completer delegate) { + this.delegate = delegate; + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + if (delegate == null) { + return; + } + try { + delegate.complete(reader, line, candidates); + } catch (Throwable t) { + // ignore + } + } +} \ No newline at end of file diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 7d83c41b8a..31da7d7f6e 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -188,8 +188,7 @@ private static void startInteractiveShell() throws Exception { Completer combinedCompleter = new AggregateCompleter( systemRegistry.completer(), - engine.getScriptCompleter() - ); + new SafeCompleter(engine.getScriptCompleter())); // 5. Build the LineReader LineReader reader = LineReaderBuilder.builder() From b50ee89f7ca4d8400ea726b1f5493db3ac57ee75 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:38:57 +0200 Subject: [PATCH 37/40] KNOX-3278: correct knoxshell.sh to use launcher and correctly load classes from the lib directory. --- gateway-shell-release/home/bin/knoxshell.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-shell-release/home/bin/knoxshell.sh b/gateway-shell-release/home/bin/knoxshell.sh index 877441502c..4b54aa177b 100755 --- a/gateway-shell-release/home/bin/knoxshell.sh +++ b/gateway-shell-release/home/bin/knoxshell.sh @@ -81,7 +81,7 @@ function main { checkJava buildAppJavaOpts - $JAVA "${APP_JAVA_OPTS[@]}" -Dlog4j.configurationFile=conf/knoxshell-log4j2.xml -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -cp "$APP_JAR":lib/* -jar "$APP_JAR" "$@" || exit 1 + $JAVA "${APP_JAVA_OPTS[@]}" -Dlog4j.configurationFile=conf/knoxshell-log4j2.xml -javaagent:"$APP_BIN_DIR"/../lib/aspectjweaver.jar -cp "$APP_JAR":lib/* -cp "$APP_JAR":lib/* org.apache.knox.gateway.launcher.Launcher "$@" || exit 1 return 0 } From 6d85aae3552a19ad84c68d007dd3fca3fde17c12 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:11:04 +0200 Subject: [PATCH 38/40] KNOX-3278: fix no endline at end of file. --- .../org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java | 2 +- .../main/java/org/apache/knox/gateway/shell/SafeCompleter.java | 2 +- .../org/apache/knox/gateway/shell/commands/ImportCommand.java | 2 +- .../org/apache/knox/gateway/shell/commands/PurgeCommand.java | 2 +- .../org/apache/knox/gateway/shell/commands/ShowCommand.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java index a401b67172..a5c077ce7c 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java @@ -105,4 +105,4 @@ private List getCompletersForCommand(String command) { } return Collections.singletonList(NullCompleter.INSTANCE); } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java index fb59eda3ad..35e9b29097 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/SafeCompleter.java @@ -46,4 +46,4 @@ public void complete(LineReader reader, ParsedLine line, List candida // ignore } } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java index ecee87b95d..a0145af3a5 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java @@ -90,4 +90,4 @@ public Object execute(List args) { terminal.writer().flush(); return null; } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java index 92122bc475..1d2564df7c 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java @@ -131,4 +131,4 @@ public List getCompleters() { Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all"); return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE); } -} \ No newline at end of file +} diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java index aba386fba8..7879868a58 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java @@ -108,4 +108,4 @@ public List getCompleters() { return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE); } -} \ No newline at end of file +} From 0533c3d73cad869b4e5b5b8c9f3cbf74ad1b9990 Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:46:41 +0200 Subject: [PATCH 39/40] KNOX-3278: refactor Shell.java pt1 --- .../org/apache/knox/gateway/shell/Shell.java | 186 +++++++++++------- 1 file changed, 114 insertions(+), 72 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index 31da7d7f6e..f748a94684 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -21,6 +21,7 @@ import org.apache.groovy.groovysh.jline.SystemRegistryImpl; import org.apache.knox.gateway.shell.commands.AbstractKnoxShellCommand; +import org.apache.knox.gateway.shell.commands.AbstractSQLCommandSupport; import org.apache.knox.gateway.shell.commands.CSVCommand; import org.apache.knox.gateway.shell.commands.DataSourceCommand; import org.apache.knox.gateway.shell.commands.ImportCommand; @@ -115,41 +116,105 @@ private static void startInteractiveShell() throws Exception { } // 3. Instantiate and Map Custom Commands + List commands = createCommands(engine, terminal); + Map registry = createRegistry(commands); + + Map commandMethods = createCommandMethods(commands); + Map commandAliases = createCommandAliases(commands); + + DefaultParser parser = new DefaultParser(); + // Override default regex to allow '.' as a valid command string + // Original: "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*" + parser.setRegexCommand("(?:\\.|[:]?[a-zA-Z]+[a-zA-Z0-9_-]*)"); + Path workDir = Paths.get(System.getProperty("user.dir")); + KnoxShellCommandRegistry knoxShellCommandRegistry = new KnoxShellCommandRegistry(commandMethods, commandAliases); + SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null); + systemRegistry.setCommandRegistries(knoxShellCommandRegistry); + SystemRegistry.add(systemRegistry); + + // 4. Setup Tab Completers for our custom commands (e.g., ":sql", ":fs") + Completer combinedCompleter = new AggregateCompleter( + systemRegistry.completer(), + new SafeCompleter(engine.getScriptCompleter())); + + // 5. Build the LineReader + LineReader reader = LineReaderBuilder.builder() + .parser(parser) + .terminal(terminal) + .completer(combinedCompleter) + .variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".knoxshell_history")) + .build(); + + terminal.writer().println("Apache Knox Shell"); + terminal.writer().println("Type ':help' (':h' or '?') for help, ':exit' or ':quit' (':x' or ':q') to quit."); + terminal.writer().flush(); + + // 6. Setup Shutdown Hook (Calling closeConnections directly on our object instances) + createShutdownHook(commands); + + // 7. The REPL Loop + runRepl(reader, terminal, registry, engine); + } + + private static List createCommands(GroovyEngine engine, Terminal terminal) { + return Arrays.asList( + new CSVCommand(engine, terminal), + new DataSourceCommand(engine, terminal), + new SelectCommand(engine, terminal), + new WebHDFSCommand(engine, terminal), + new ImportCommand(engine, terminal), + new LoadCommand(engine, terminal), + new PurgeCommand(engine, terminal), + new ShowCommand(engine, terminal) + ); + } + + private static Map createRegistry(List commands) { Map registry = new HashMap<>(); - CSVCommand csvCmd = new CSVCommand(engine, terminal); - DataSourceCommand dsCmd = new DataSourceCommand(engine, terminal); - SelectCommand selectCmd = new SelectCommand(engine, terminal); - WebHDFSCommand hdfsCmd = new WebHDFSCommand(engine, terminal); - - ImportCommand importCmd = new ImportCommand(engine, terminal); - LoadCommand loadCmd = new LoadCommand(engine, terminal); - PurgeCommand purgeCommand = new PurgeCommand(engine, terminal); - ShowCommand showCmd = new ShowCommand(engine, terminal); - - registerCommand(registry, importCmd); - registerCommand(registry, selectCmd); - registerCommand(registry, dsCmd); - registerCommand(registry, csvCmd); - registerCommand(registry, hdfsCmd); - registerCommand(registry, showCmd); - registerCommand(registry, loadCmd); - registerCommand(registry, purgeCommand); + if (commands == null || commands.isEmpty()) { + return registry; + } - Map commandMethods = new HashMap<>(); - Map commandAliases = new HashMap<>(); + for (AbstractKnoxShellCommand cmd : commands) { + registerCommand(registry, cmd); + } + return registry; + } - registry.forEach((name, cmd) -> { - if (name.equals(cmd.getShortcut())) { - return; - } - String shortcut = cmd.getShortcut(); - if (shortcut != null && !shortcut.isEmpty()) { - commandAliases.put(shortcut, name); + + private static void createShutdownHook(List commands) { + if (commands == null || commands.isEmpty()) { + return; + } + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\nClosing any open connections..."); + + for (AbstractKnoxShellCommand cmd : commands) { + // Check if the command inherits from the SQL base class + if (cmd instanceof AbstractSQLCommandSupport) { + ((AbstractSQLCommandSupport) cmd).closeConnections(); } + } + })); + } + + private static void registerCommand(Map registry, AbstractKnoxShellCommand cmd) { + registry.put(cmd.getName(), cmd); + if (cmd.getShortcut() != null && !cmd.getShortcut().isEmpty()) { + registry.put(cmd.getShortcut(), cmd); + } + } + + private static Map createCommandMethods(List commands) { + Map commandMethods = new HashMap<>(); - // 2. Put them in the map with the colon-prefixed names - commandMethods.put(name, new CommandMethods( + if (commands == null || commands.isEmpty()) { + return commandMethods; + } + + for (AbstractKnoxShellCommand cmd : commands) { + commandMethods.put(cmd.getName(), new CommandMethods( (input) -> { try { String[] allTokens = input.args(); @@ -171,51 +236,34 @@ private static void startInteractiveShell() throws Exception { : Collections.singletonList(NullCompleter.INSTANCE); } )); - }); - - DefaultParser parser = new DefaultParser(); - // Override default regex to allow '.' as a valid command string - // Original: "[:]?[a-zA-Z]+[a-zA-Z0-9_-]*" - parser.setRegexCommand("(?:\\.|[:]?[a-zA-Z]+[a-zA-Z0-9_-]*)"); - Path workDir = Paths.get(System.getProperty("user.dir")); - KnoxShellCommandRegistry knoxShellCommandRegistry = new KnoxShellCommandRegistry(commandMethods, commandAliases); - SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> workDir, null); - systemRegistry.setCommandRegistries(knoxShellCommandRegistry); - SystemRegistry.add(systemRegistry); - - // 4. Setup Tab Completers - // StringsCompleter automatically suggests our custom commands (e.g., ":sql", ":fs") + } - Completer combinedCompleter = new AggregateCompleter( - systemRegistry.completer(), - new SafeCompleter(engine.getScriptCompleter())); + return commandMethods; + } - // 5. Build the LineReader - LineReader reader = LineReaderBuilder.builder() - .parser(parser) - .terminal(terminal) - .completer(combinedCompleter) - .variable(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".knoxshell_history")) - .build(); + private static Map createCommandAliases(List commands) { + Map commandAliases = new HashMap<>(); - terminal.writer().println("Apache Knox Shell"); - terminal.writer().println("Type ':help' (':h' or '?') for help, ':exit' or ':quit' (':x' or ':q') to quit."); - terminal.writer().flush(); + if (commands == null || commands.isEmpty()) { + return commandAliases; + } - // 6. Setup Shutdown Hook (Calling closeConnections directly on our object instances) - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - System.out.println("\nClosing any open connections..."); - dsCmd.closeConnections(); - selectCmd.closeConnections(); - })); + for (AbstractKnoxShellCommand cmd : commands) { + String shortcut = cmd.getShortcut(); + if (shortcut != null && !shortcut.isEmpty()) { + commandAliases.put(shortcut, cmd.getName()); + } + } + return commandAliases; + } - // 7. The REPL Loop + private static void runRepl(LineReader reader, Terminal terminal, Map registry, GroovyEngine engine) { while (true) { try { String line = reader.readLine("knox> "); if (line == null) { - break; + return; } String trimmed = line.trim(); @@ -225,7 +273,7 @@ private static void startInteractiveShell() throws Exception { // --- BUILT-IN COMMANDS --- if (EXIT_COMMANDS.stream().anyMatch(trimmed::equalsIgnoreCase)) { - break; + return; // Exits the method, allowing main() to finish cleanly } if (HELP_COMMANDS.stream().anyMatch(h -> trimmed.equalsIgnoreCase(h) || trimmed.startsWith(h + " "))) { @@ -290,7 +338,7 @@ private static void startInteractiveShell() throws Exception { } catch (UserInterruptException | EndOfFileException e) { // Ctrl+C or Ctrl+D cleanly exits the shell - break; + return; } catch (Throwable e) { // Shell should not exit (similar to legacy GroovySh) terminal.writer().println("Error: " + e.getMessage()); @@ -299,10 +347,4 @@ private static void startInteractiveShell() throws Exception { } } - private static void registerCommand(Map registry, AbstractKnoxShellCommand cmd) { - registry.put(cmd.getName(), cmd); - if (cmd.getShortcut() != null && !cmd.getShortcut().isEmpty()) { - registry.put(cmd.getShortcut(), cmd); - } - } } From 543095e767fe8ba017d6ed8fa5a6c5aacd13c83f Mon Sep 17 00:00:00 2001 From: bonampak <14160522+bonampak@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:09:55 +0200 Subject: [PATCH 40/40] KNOX-3278: refactor Shell.java pt2 --- .../org/apache/knox/gateway/shell/Shell.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java index f748a94684..6aff31755b 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/Shell.java @@ -101,11 +101,11 @@ public static void main(String... args) throws Exception { } } else { // Boot the Interactive JLine 3 REPL - startInteractiveShell(); + new Shell().startInteractiveShell(); } } - private static void startInteractiveShell() throws Exception { + private void startInteractiveShell() throws Exception { // 1. Build Terminal and Engine Terminal terminal = TerminalBuilder.builder().system(true).name("KnoxShell").build(); GroovyEngine engine = new GroovyEngine(); @@ -156,7 +156,7 @@ private static void startInteractiveShell() throws Exception { runRepl(reader, terminal, registry, engine); } - private static List createCommands(GroovyEngine engine, Terminal terminal) { + private List createCommands(GroovyEngine engine, Terminal terminal) { return Arrays.asList( new CSVCommand(engine, terminal), new DataSourceCommand(engine, terminal), @@ -169,7 +169,7 @@ private static List createCommands(GroovyEngine engine ); } - private static Map createRegistry(List commands) { + private Map createRegistry(List commands) { Map registry = new HashMap<>(); if (commands == null || commands.isEmpty()) { return registry; @@ -182,8 +182,7 @@ private static Map createRegistry(List commands) { + private void createShutdownHook(List commands) { if (commands == null || commands.isEmpty()) { return; } @@ -199,14 +198,14 @@ private static void createShutdownHook(List commands) })); } - private static void registerCommand(Map registry, AbstractKnoxShellCommand cmd) { + private void registerCommand(Map registry, AbstractKnoxShellCommand cmd) { registry.put(cmd.getName(), cmd); if (cmd.getShortcut() != null && !cmd.getShortcut().isEmpty()) { registry.put(cmd.getShortcut(), cmd); } } - private static Map createCommandMethods(List commands) { + private Map createCommandMethods(List commands) { Map commandMethods = new HashMap<>(); if (commands == null || commands.isEmpty()) { @@ -241,7 +240,7 @@ private static Map createCommandMethods(List createCommandAliases(List commands) { + private Map createCommandAliases(List commands) { Map commandAliases = new HashMap<>(); if (commands == null || commands.isEmpty()) { @@ -258,7 +257,7 @@ private static Map createCommandAliases(List registry, GroovyEngine engine) { + private void runRepl(LineReader reader, Terminal terminal, Map registry, GroovyEngine engine) { while (true) { try { String line = reader.readLine("knox> ");