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-release/home/bin/knoxshell.sh b/gateway-shell-release/home/bin/knoxshell.sh index 14cea81e79..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/* 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/* -cp "$APP_JAR":lib/* org.apache.knox.gateway.launcher.Launcher "$@" || exit 1 return 0 } diff --git a/gateway-shell-release/home/conf/knoxshell-log4j2.xml b/gateway-shell-release/home/conf/knoxshell-log4j2.xml index c8ed33c6e6..275094a344 100644 --- a/gateway-shell-release/home/conf/knoxshell-log4j2.xml +++ b/gateway-shell-release/home/conf/knoxshell-log4j2.xml @@ -15,10 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. --> - - logs + ${sys:launcher.dir}/../logs ${sys:launcher.name}.log diff --git a/gateway-shell-release/pom.xml b/gateway-shell-release/pom.xml index e8b872b011..0724c460a0 100644 --- a/gateway-shell-release/pom.xml +++ b/gateway-shell-release/pom.xml @@ -43,8 +43,15 @@ false + + META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule + + org.apache.knox.gateway.launcher.Launcher + + true + @@ -54,6 +61,7 @@ schema/** **/*.ldif + META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat diff --git a/gateway-shell/pom.xml b/gateway-shell/pom.xml index ac24cba33e..bb718e8c40 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 @@ -69,20 +63,32 @@ groovy-json - org.fusesource.jansi + org.apache.httpcomponents + httpcore + + + org.apache.httpcomponents + httpclient + + + org.jline jansi org.jline - jline + jline-builtins - org.apache.httpcomponents - httpcore + org.jline + jline-console - org.apache.httpcomponents - httpclient + org.jline + jline-reader + + + org.jline + jline-terminal net.minidev 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/KnoxShellCommandRegistry.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java new file mode 100644 index 0000000000..a5c077ce7c --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxShellCommandRegistry.java @@ -0,0 +1,108 @@ +/* + * 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.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class KnoxShellCommandRegistry implements CommandRegistry { + + private final Map commands; + private final 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); + } + + @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); + 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); + } +} 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..35e9b29097 --- /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 + } + } +} 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..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 @@ -19,10 +19,16 @@ 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.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; +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; import org.apache.knox.gateway.shell.hbase.HBase; import org.apache.knox.gateway.shell.hdfs.Hdfs; @@ -31,19 +37,41 @@ 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.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.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 { 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 List HELP_COMMANDS = Arrays.asList(":help", ":h", "?"); + private static final String[] IMPORTS = new String[] { KnoxSession.class.getName(), HBase.class.getName(), @@ -56,47 +84,265 @@ 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 + new Shell().startInteractiveShell(); + } + } + + private 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 + 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 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 Map createRegistry(List commands) { + Map registry = new HashMap<>(); + if (commands == null || commands.isEmpty()) { + return registry; + } + + for (AbstractKnoxShellCommand cmd : commands) { + registerCommand(registry, cmd); + } + + return registry; + } + + private 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(); } - }); - for( String name : IMPORTS ) { - shell.execute( "import " + name ); } - // 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 void registerCommand(Map registry, AbstractKnoxShellCommand cmd) { + registry.put(cmd.getName(), cmd); + if (cmd.getShortcut() != null && !cmd.getShortcut().isEmpty()) { + registry.put(cmd.getShortcut(), cmd); + } + } + + private Map createCommandMethods(List commands) { + Map commandMethods = new HashMap<>(); + + if (commands == null || commands.isEmpty()) { + return commandMethods; + } + + for (AbstractKnoxShellCommand cmd : commands) { + commandMethods.put(cmd.getName(), new CommandMethods( + (input) -> { + try { + String[] allTokens = input.args(); + // 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) { + input.session().terminal().writer().println("Error: " + e.getMessage()); + return null; + } + }, + (line) -> { + List completers = cmd.getCompleters(); + return (completers != null && !completers.isEmpty()) + ? completers + : Collections.singletonList(NullCompleter.INSTANCE); + } + )); + } + + return commandMethods; + } + + private Map createCommandAliases(List commands) { + Map commandAliases = new HashMap<>(); + + if (commands == null || commands.isEmpty()) { + return commandAliases; + } + + for (AbstractKnoxShellCommand cmd : commands) { + String shortcut = cmd.getShortcut(); + if (shortcut != null && !shortcut.isEmpty()) { + commandAliases.put(shortcut, cmd.getName()); + } + } + + return commandAliases; + } + + private void runRepl(LineReader reader, Terminal terminal, Map registry, GroovyEngine engine) { + while (true) { + try { + String line = reader.readLine("knox> "); + if (line == null) { + return; + } + + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + + // --- BUILT-IN COMMANDS --- + if (EXIT_COMMANDS.stream().anyMatch(trimmed::equalsIgnoreCase)) { + return; // Exits the method, allowing main() to finish cleanly + } + + if (HELP_COMMANDS.stream().anyMatch(h -> trimmed.equalsIgnoreCase(h) || 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(Locale.ROOT, " %-25s %s%n", names, desc); + }); + + 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, :x, :quit, :q", "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]; + + 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); + } + } 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 + return; + } catch (Throwable e) { + // Shell should not exit (similar to legacy GroovySh) + terminal.writer().println("Error: " + e.getMessage()); + terminal.writer().flush(); + } } } 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..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 @@ -17,47 +17,67 @@ */ 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.apache.groovy.groovysh.CommandSupport; -import org.apache.groovy.groovysh.Groovysh; +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 { + + protected final GroovyEngine engine; + protected final Terminal terminal; + private final String name; + private final String shortcut; -public abstract class AbstractKnoxShellCommand extends CommandSupport { - static final String KNOXSQLHISTORY = "__knoxsqlhistory"; - protected static final String KNOXDATASOURCES = "__knoxdatasources"; private String description; private String usage; private String help; - public AbstractKnoxShellCommand(Groovysh shell, String name, String shortcut) { - super(shell, name, shortcut); - } - - public AbstractKnoxShellCommand(Groovysh shell, String name, String shortcut, - String desc, String usage, String help) { - super(shell, name, shortcut); + 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 + 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; } + public List getCompleters() { + return Collections.singletonList(NullCompleter.INSTANCE); + } + + public abstract Object execute(List args) throws Exception; + protected String getBindingVariableNameForResultingTable(List args) { String variableName = null; boolean nextOne = false; @@ -74,8 +94,70 @@ 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(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..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 @@ -28,79 +28,79 @@ 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 { + protected static final String KNOXDATASOURCES = "__knoxdatasources"; 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); - } + private static final String KNOXSQLHISTORY = "__knoxsqlhistory"; + private static final String KNOXDATASOURCE_CONNECTIONS = "__knoxdatasourceconnections"; - 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(KNOXDATASOURCE_CONNECTIONS); + if (connections == null) { + connections = new HashMap<>(); + } + return connections.get(ds.getName()); } @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) { conn = JDBCUtils.createConnection(ds.getConnectStr(), user, pass); - } - else { + } else { conn = JDBCUtils.createConnection(ds.getConnectStr(), null, null); + } + HashMap connections = (HashMap) engine.get(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(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 +120,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 +136,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,21 +173,23 @@ 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) { + 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<>(); } } @@ -191,18 +199,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..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 @@ -20,32 +20,43 @@ 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); + 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); + 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); + + 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); } @@ -53,30 +64,32 @@ public Object execute(List args) { if (withHeaders) { if (url.startsWith("$")) { // a knoxshell variable is a csv file as a string - String csvString = (String) getVariables().get(url.substring(1)); + 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)); + 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(); + terminal.writer().println("Error parsing CSV: " + e.getMessage()); + e.printStackTrace(terminal.writer()); + terminal.writer().flush(); } + if (table != null && bindVariableName != null) { - getVariables().put(bindVariableName, table); + engine.put(bindVariableName, table); + terminal.writer().println("Assigned resulting table to variable: " + bindVariableName); + terminal.writer().flush(); } + return table; } - } 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..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 @@ -20,68 +20,92 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; +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; import org.apache.knox.gateway.shell.KnoxDataSource; import org.apache.knox.gateway.shell.table.KnoxShellTable; -import org.apache.groovy.groovysh.Groovysh; + +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; 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); + 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(); - 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(); + + String action = (args == null || args.isEmpty()) ? "list" : args.get(0); + + 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"); + 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); persistDataSources(); } - else if (args.get(0).equalsIgnoreCase("remove")) { + else if ("remove".equalsIgnoreCase(action)) { if (dataSources == null || dataSources.isEmpty()) { return "No datasources to remove."; } + if (args.size() < 2) { + terminal.writer().println("Error: Missing datasource name to remove."); + terminal.writer().flush(); + return null; + } + + String dsName = args.get(1); // 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."); + dataSources.remove(dsName); + + if (engine.get(KNOXDATASOURCE) != null) { + if ((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 ("list".equalsIgnoreCase(action)) { // valid command no additional work needed though - } - else if(args.get(0).equalsIgnoreCase("select")) { + } else if ("select".equalsIgnoreCase(action)) { if (dataSources == null || dataSources.isEmpty()) { return "No datasources to select from."; } + 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,33 +116,40 @@ 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)); + 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)); } + KnoxShellTable datasource = new KnoxShellTable(); datasource.title("Knox DataSource Selected"); 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 { - return "ERROR: unknown datasources command."; + } else { + return "ERROR: unknown datasources command: " + action; } return buildTable(); @@ -128,20 +159,87 @@ 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); + 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()); } } 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 = 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() { + 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) { - 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(); + } } } 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..a0145af3a5 --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ImportCommand.java @@ -0,0 +1,93 @@ +/* + * 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.Map; +import java.util.regex.Pattern; + +/** + * 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 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); + } + + @Override + public Object execute(List args) { + if (args == null || args.isEmpty()) { + // List mode + Map imports = engine.getImports(); + if (imports.isEmpty()) { + terminal.writer().println("No imports registered."); + } else { + terminal.writer().println("Active imports:"); + imports.values().stream() + .sorted() + .forEach(value -> terminal.writer().println(value)); } + terminal.writer().flush(); + return null; + } + + // Join with spaces to preserve "static" keyword + String target = String.join(" ", args).trim(); + + 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); + } catch (Exception e) { + terminal.writer().println("Failed to import '" + target + "': " + e.getMessage()); + } + + terminal.writer().flush(); + return null; + } +} 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(); - } - } -} 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..f621d32852 --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/LoadCommand.java @@ -0,0 +1,203 @@ +/* + * 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.builtins.Completers; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.NullCompleter; +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.Arrays; +import java.util.List; + +/** + * 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; + } + + Object lastResult = 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 + } + + // 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 = ""; + } + } + + if (script.trim().isEmpty()) { + terminal.writer().println("Warning: '" + location + "' is empty, nothing to execute."); + terminal.writer().flush(); + continue; + } + + terminal.writer().println("Loading " + location + " ..."); + terminal.writer().flush(); + + 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 { + // 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()); + } + + try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + return readAndSkipShebang(reader); + } + } + + private boolean isUrl(String location) { + return location!=null && + (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 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 + 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); + } + +} 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..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 @@ -21,38 +21,89 @@ 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 { + + public LoginCommand(GroovyEngine engine, Terminal terminal) { + 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) { + 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); + 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; } 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 { + 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(); + } } } 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..1d2564df7c --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/PurgeCommand.java @@ -0,0 +1,134 @@ +/* + * 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.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; + +/** + * 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\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"; + + /** + * @param engine the GroovyEngine + * @param terminal the JLine terminal + */ + public PurgeCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); + } + + @Override + public Object execute(List args) { + String what = (args == null || args.isEmpty()) ? "variables" : args.get(0).toLowerCase(Locale.ROOT); + + switch (what) { + case "variables": + int varCount = clearVariables(); + terminal.writer().println("Purged " + varCount + " variable(s). Internal Knox state preserved."); + break; + case "imports": + int importCount = clearImports(); + terminal.writer().println("Purged " + importCount + " import(s)."); + break; + case "all": + int vc = clearVariables(); + int ic = clearImports(); + terminal.writer().println("Purged " + vc + " variable(s) and " + ic + " import(s)."); + break; + default: + terminal.writer().println(USAGE); + break; + } + + terminal.writer().flush(); + return null; + } + + private int clearVariables() { + java.util.Map variables = engine.find(); + if (variables == null || variables.isEmpty()) { + return 0; + } + + int count = 0; + List keysToDelete = new java.util.ArrayList<>(); + for (String variableName : variables.keySet()) { + // Preserve internal Knox bindings + 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 clearImports() { + Map imports = engine.getImports(); + + if (imports == null || imports.isEmpty()) { + return 0; + } + + int count = 0; + for (String importName : imports.keySet()) { + engine.removeImport(importName); + count++; + } + return count; + } + + @Override + public List getCompleters() { + Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all"); + return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE); + } +} 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..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 @@ -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, @@ -26,17 +26,19 @@ import java.util.List; import java.util.Map; +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.jline.GroovyEngine; +import org.jline.terminal.Terminal; + 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 { private static final String USAGE = ":sql [assign resulting-variable-name]"; private static final String DESC = "Build table from SQL ResultSet"; @@ -46,8 +48,8 @@ public class SelectCommand extends AbstractSQLCommandSupport implements KeyListe private List sqlHistory; private int historyIndex = -1; - public SelectCommand(Groovysh shell) { - super(shell, ":SQL", ":sql", DESC, USAGE, DESC); + public SelectCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, ":SQL", ":sql", DESC, USAGE, DESC); } @Override @@ -59,14 +61,14 @@ public void keyPressed(KeyEvent event) { historyIndex = sqlHistory.size() + 1; } if (code == KeyEvent.VK_KP_UP || - code == KeyEvent.VK_UP) { + code == KeyEvent.VK_UP) { if (historyIndex > 0) { historyIndex -= 1; } setFromHistory = true; } else if (code == KeyEvent.VK_KP_DOWN || - code == KeyEvent.VK_DOWN) { + code == KeyEvent.VK_DOWN) { if (historyIndex < sqlHistory.size() - 1) { historyIndex += 1; setFromHistory = true; @@ -87,7 +89,7 @@ public void keyReleased(KeyEvent event) { public void keyTyped(KeyEvent event) { } - @SuppressWarnings({"unchecked", "PMD.CloseResource"}) + @SuppressWarnings({"PMD.CloseResource"}) @Override public Object execute(List args) { boolean ok = false; @@ -95,29 +97,28 @@ public Object execute(List args) { 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; + KnoxDataSource ds; + 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}'."; } } + ds = dataSources.get(dsName); 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); @@ -132,7 +133,7 @@ else if (dataSources.size() == 1) { SwingUtils.workAroundFocusIssue(sqlField); int x = JOptionPane.showConfirmDialog(null, box, - "SQL Query Input", JOptionPane.OK_CANCEL_OPTION); + "SQL Query Input", JOptionPane.OK_CANCEL_OPTION); if (x == JOptionPane.OK_OPTION) { ok = true; @@ -141,6 +142,7 @@ else if (dataSources.size() == 1) { historyIndex = -1; } + //KnoxShellTable.builder().jdbc().connect("jdbc:derby:codejava/webdb1").driver("org.apache.derby.jdbc.EmbeddedDriver").username("lmccay").pwd("xxxx").sql("SELECT * FROM book"); try { if (ok) { @@ -155,7 +157,8 @@ else if (dataSources.size() == 1) { username = dlg.name(); pass = dlg.chars(); } - conn = getConnection(ds, username, new String(pass)); + String passStr = (pass == null) ? null : new String(pass); + conn = getConnection(ds, username, passStr); } try (Statement statement = conn.createStatement()) { if (statement.execute(sql)) { @@ -164,22 +167,26 @@ else if (dataSources.size() == 1) { } } } - } - catch (SQLException e) { - System.out.println("SQL Exception encountered... " + e.getMessage()); + } 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); + terminal.writer().println("Assigned resulting table to variable: " + bindVariableName); + terminal.writer().flush(); } + return table; } + } 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..7879868a58 --- /dev/null +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/commands/ShowCommand.java @@ -0,0 +1,111 @@ +/* + * 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.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; + +/** + * 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, imports or both"; + 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"; + + public ShowCommand(GroovyEngine engine, Terminal terminal) { + super(engine, terminal, NAME, SHORTCUT, DESC, USAGE, HELP); + } + + @Override + public Object execute(List args) { + String what = (args == null || args.isEmpty()) ? "variables" : args.get(0).toLowerCase(Locale.ROOT); + + switch (what) { + case "variables": + 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.find(); + 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.toString() : "null"; + terminal.writer().printf(Locale.ROOT, " %-25s (%s) = %s%n", name, type, display); + }); + } + + private void showImports() { + java.util.Set imports = engine.getImports().keySet(); + if (imports.isEmpty()) { + terminal.writer().println("No imports registered."); + } else { + terminal.writer().println("Imports:"); + imports.forEach(i -> terminal.writer().println(" import " + i)); + } + } + + @Override + public List getCompleters() { + Completer subCommandCompleter = new StringsCompleter("variables", "imports", "all"); + return Arrays.asList(subCommandCompleter, NullCompleter.INSTANCE); + } + +} 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..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 @@ -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,157 @@ 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); + 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 ("mount".equalsIgnoreCase(action)) { + 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 ("unmount".equalsIgnoreCase(action)) { + 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 ("mounts".equalsIgnoreCase(action)) { return listMounts(mounts); } - else if (args.get(0).equalsIgnoreCase("ls")) { - String path = args.get(1); - return listStatus(mounts, path); + else if ("ls".equalsIgnoreCase(action)) { + if (args == null || args.size() < 2) { + return printError("Usage: :fs ls "); + } else { + return listStatus(mounts, args.get(1)); + } } - else if (args.get(0).equalsIgnoreCase("put")) { + 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); 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")) { + else if ("rm".equalsIgnoreCase(action)) { // Hdfs.rm( session ).file( dataFile ).now() // :fs rm target-path - String path = args.get(1); - return remove(mounts, path); + if (args.size() < 2) { + return printError("Usage: :fs rm "); + } + return remove(mounts, args.get(1)); } - else if (args.get(0).equalsIgnoreCase("cat")) { + else if ("cat".equalsIgnoreCase(action)) { // println Hdfs.get( session ).from( dataDir + "/" + dataFile ).now().string // :fs cat target-path - String path = args.get(1); - return cat(mounts, path); + if (args.size() < 2) { + return printError("Usage: :fs cat "); + } + return cat(mounts, args.get(1)); } - else if (args.get(0).equalsIgnoreCase("mkdir")) { + else if ("mkdir".equalsIgnoreCase(action)) { // 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); + if (args.size() < 2) { + return printError("Usage: :fs mkdir [perms]"); } - - return mkdir(mounts, path, perms); + String perms = (args.size() == 3) ? args.get(2) : null; + return mkdir(mounts, args.get(1), perms); } - else if (args.get(0).equalsIgnoreCase("get")) { + 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); 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 +195,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(); + Hdfs.mkdir(sessions.get(mountPoint)).dir(targetPath).perm(perms).now().getString(); + } else { + Hdfs.mkdir(session).dir(targetPath).now().getString(); } - else { - Hdfs.mkdir(session).dir(targetPath).perm(perms).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 { - result = targetPath + " already exists"; + } else { + return 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 +236,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 +253,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 +351,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 +392,15 @@ private String stripMountPoint(String path, String mountPoint) { } private String determineMountPoint(String path) { - String mountPoint = null; - if (path.startsWith("/")) { + 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("/"); - mountPoint = pathElements[1]; + if (pathElements.length > 1) { + return pathElements[1]; + } } - return mountPoint; + return null; } private KnoxShellTable buildTableFromListStatus(String directory, List> list) { @@ -394,32 +414,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(); + } } } 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/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/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 @@ - + 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 +} 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..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 @@ -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)); // selected the first column (ZIP) where the last element - index 13 - is 14 } 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" diff --git a/pom.xml b/pom.xml index a9c4cc46eb..7512f56aa9 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 @@ -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 @@ -223,7 +223,6 @@ 5.3.6 2.18.2 0.8.13 - 1.18 1.2.1 1.2.2 1.3.2 @@ -240,8 +239,8 @@ 3.4 2.47 9.4.57.v20241219 - 3.21.0 - 5.9.0 + 3.30.6 + 5.18.1 2.10.8 2.9.0 2.5.2 @@ -272,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 @@ -1653,14 +1652,28 @@ - org.fusesource.jansi + org.jline jansi - ${jansi.version} + ${jline.version} + + + org.jline + jline-builtins + ${jline.version} + + + org.jline + jline-console + ${jline.version} + + + org.jline + jline-reader + ${jline.version} - org.jline - jline + jline-terminal ${jline.version}