From f7fbc84f13fb8eb3d13a3b522bd3d8f28df19eeb Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 29 Jul 2021 16:46:51 +0200 Subject: [PATCH] Implement the InteractiveCLI using Jline2 and picocli --- leshan-client-demo/logback-config.xml | 4 +- leshan-client-demo/pom.xml | 7 +- .../demo/cli/interactive/InteractiveCLI.java | 135 +++++--------- .../cli/interactive/InteractiveCommands.java | 169 ++++++++++++++++++ .../cli/interactive/TerminalAppender.java | 46 +++++ pom.xml | 5 + 6 files changed, 273 insertions(+), 93 deletions(-) create mode 100644 leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/InteractiveCommands.java create mode 100644 leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/TerminalAppender.java diff --git a/leshan-client-demo/logback-config.xml b/leshan-client-demo/logback-config.xml index 81f34d5fae..3997ac9709 100644 --- a/leshan-client-demo/logback-config.xml +++ b/leshan-client-demo/logback-config.xml @@ -15,14 +15,14 @@ Contributors: Sierra Wireless - initial API and implementation --> - + %d %p %C{0} - %m%n - + diff --git a/leshan-client-demo/pom.xml b/leshan-client-demo/pom.xml index c80ff955e5..dfda1829d7 100644 --- a/leshan-client-demo/pom.xml +++ b/leshan-client-demo/pom.xml @@ -35,12 +35,13 @@ Contributors: info.picocli picocli - - + + info.picocli + picocli-shell-jline2 + ch.qos.logback logback-classic - runtime diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/InteractiveCLI.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/InteractiveCLI.java index 2ef5502989..251c7fa8ed 100644 --- a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/InteractiveCLI.java +++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/InteractiveCLI.java @@ -16,109 +16,68 @@ package org.eclipse.leshan.client.demo.cli.interactive; import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Scanner; import org.eclipse.leshan.client.californium.LeshanClient; -import org.eclipse.leshan.client.demo.MyLocation; -import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; -import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; -import org.eclipse.leshan.client.resource.ObjectEnabler; -import org.eclipse.leshan.client.resource.ObjectsInitializer; -import org.eclipse.leshan.core.LwM2mId; import org.eclipse.leshan.core.model.LwM2mModel; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class InteractiveCLI { +import ch.qos.logback.classic.Logger; +import ch.qos.logback.core.Appender; +import jline.TerminalFactory; +import jline.TerminalFactory.Type; +import jline.console.ConsoleReader; +import jline.console.completer.ArgumentCompleter.ArgumentList; +import jline.console.completer.ArgumentCompleter.WhitespaceArgumentDelimiter; +import jline.internal.Configuration; +import picocli.CommandLine; +import picocli.CommandLine.Help; +import picocli.shell.jline2.PicocliJLineCompleter; - private static final Logger LOG = LoggerFactory.getLogger(InteractiveCLI.class); +public class InteractiveCLI { - private LeshanClient client; - private LwM2mModel model; + private ConsoleReader console; + private CommandLine commandLine; public InteractiveCLI(LeshanClient client, LwM2mModel model) throws IOException { - this.client = client; - this.model = model; + + // JLine 2 does not detect some terminal as not ANSI compatible, like Eclipse Console + // see : https://github.com/jline/jline2/issues/185 + // So use picocli heuristic instead : + if (!Help.Ansi.AUTO.enabled() && // + Configuration.getString(TerminalFactory.JLINE_TERMINAL, TerminalFactory.AUTO).toLowerCase() + .equals(TerminalFactory.AUTO)) { + TerminalFactory.configure(Type.NONE); + } + + // Create Interactive Shell + console = new ConsoleReader(); + console.setPrompt(""); + + // set up the completion + InteractiveCommands commands = new InteractiveCommands(console, client, model); + commandLine = new CommandLine(commands); + console.addCompleter(new PicocliJLineCompleter(commandLine.getCommandSpec())); + + // Configure Terminal appender if it is present. + Appender appender = ((Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME)) + .getAppender("TERMINAL"); + if (appender instanceof TerminalAppender) { + ((TerminalAppender) appender).setConsole(console); + } } public void showHelp() { - // Print commands help - StringBuilder commandsHelp = new StringBuilder("Commands available :"); - commandsHelp.append(System.lineSeparator()); - commandsHelp.append(System.lineSeparator()); - commandsHelp.append(" - create : to enable a new object."); - commandsHelp.append(System.lineSeparator()); - commandsHelp.append(" - delete : to disable a new object."); - commandsHelp.append(System.lineSeparator()); - commandsHelp.append(" - update : to trigger a registration update."); - commandsHelp.append(System.lineSeparator()); - commandsHelp.append(" - w : to move to North."); - commandsHelp.append(System.lineSeparator()); - commandsHelp.append(" - a : to move to East."); - commandsHelp.append(System.lineSeparator()); - commandsHelp.append(" - s : to move to South."); - commandsHelp.append(System.lineSeparator()); - commandsHelp.append(" - d : to move to West."); - commandsHelp.append(System.lineSeparator()); - LOG.info(commandsHelp.toString()); + commandLine.usage(commandLine.getOut()); } public void start() throws IOException { - // Change the location through the Console - try (Scanner scanner = new Scanner(System.in)) { - List wasdCommands = Arrays.asList('w', 'a', 's', 'd'); - while (scanner.hasNext()) { - String command = scanner.next(); - if (command.startsWith("create")) { - try { - int objectId = scanner.nextInt(); - if (client.getObjectTree().getObjectEnabler(objectId) != null) { - LOG.info("Object {} already enabled.", objectId); - } - if (model.getObjectModel(objectId) == null) { - LOG.info("Unable to enable Object {} : there no model for this.", objectId); - } else { - ObjectsInitializer objectsInitializer = new ObjectsInitializer(model); - objectsInitializer.setDummyInstancesForObject(objectId); - LwM2mObjectEnabler object = objectsInitializer.create(objectId); - client.getObjectTree().addObjectEnabler(object); - } - } catch (Exception e) { - // skip last token - scanner.next(); - LOG.info("Invalid syntax, must be an integer : create "); - } - } else if (command.startsWith("delete")) { - try { - int objectId = scanner.nextInt(); - if (objectId == 0 || objectId == 0 || objectId == 3) { - LOG.info("Object {} can not be disabled.", objectId); - } else if (client.getObjectTree().getObjectEnabler(objectId) == null) { - LOG.info("Object {} is not enabled.", objectId); - } else { - client.getObjectTree().removeObjectEnabler(objectId); - } - } catch (Exception e) { - // skip last token - scanner.next(); - LOG.info("\"Invalid syntax, must be an integer : delete "); - } - } else if (command.startsWith("update")) { - client.triggerRegistrationUpdate(); - } else if (command.length() == 1 && wasdCommands.contains(command.charAt(0))) { - LwM2mObjectEnabler objectEnabler = client.getObjectTree().getObjectEnabler(LwM2mId.LOCATION); - if (objectEnabler != null && objectEnabler instanceof ObjectEnabler) { - LwM2mInstanceEnabler instance = ((ObjectEnabler) objectEnabler).getInstance(0); - if (instance instanceof MyLocation) { - ((MyLocation) instance).moveLocation(command); - } - } - } else { - LOG.info("Unknown command '{}'", command); - } - } + + // start the shell and process input until the user quits with Ctl-D + String line; + while ((line = console.readLine()) != null) { + ArgumentList list = new WhitespaceArgumentDelimiter().delimit(line, line.length()); + commandLine.execute(list.getArguments()); + console.killLine(); } } } diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/InteractiveCommands.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/InteractiveCommands.java new file mode 100644 index 0000000000..c63ee2c5d4 --- /dev/null +++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/InteractiveCommands.java @@ -0,0 +1,169 @@ +package org.eclipse.leshan.client.demo.cli.interactive; + +import java.io.PrintWriter; + +import org.eclipse.leshan.client.californium.LeshanClient; +import org.eclipse.leshan.client.demo.MyLocation; +import org.eclipse.leshan.client.demo.cli.interactive.InteractiveCommands.CreateCommand; +import org.eclipse.leshan.client.demo.cli.interactive.InteractiveCommands.DeleteCommand; +import org.eclipse.leshan.client.demo.cli.interactive.InteractiveCommands.MoveCommand; +import org.eclipse.leshan.client.demo.cli.interactive.InteractiveCommands.UpdateCommand; +import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.ObjectEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.core.LwM2mId; +import org.eclipse.leshan.core.model.LwM2mModel; + +import jline.console.ConsoleReader; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.HelpCommand; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +/** + * Interactive commands for the Leshan Client Demo + */ +@Command(name = "", + description = "@|bold,underline Leshan Client Demo Interactive Console :|@%n", + footer = { "%n@|italic Press Ctl-C to exit.|@%n" }, + subcommands = { HelpCommand.class, CreateCommand.class, DeleteCommand.class, UpdateCommand.class, + MoveCommand.class }, + customSynopsis = { "" }, + synopsisHeading = "") +public class InteractiveCommands implements Runnable { + + private PrintWriter out; + + private LeshanClient client; + private LwM2mModel model; + + public InteractiveCommands(ConsoleReader reader, LeshanClient client, LwM2mModel model) { + out = new PrintWriter(reader.getOutput()); + + this.client = client; + this.model = model; + } + + @Override + public void run() { + out.print(new CommandLine(this).getUsageMessage()); + out.flush(); + } + + /** + * A command to create object enabler. + */ + @Command(name = "create", description = "Enable a new Object", headerHeading = "%n", footer = "") + static class CreateCommand implements Runnable { + + @Parameters(description = "Id of the LWM2M object to enable") + private Integer objectId; + + @ParentCommand + InteractiveCommands parent; + + @Override + public void run() { + if (parent.client.getObjectTree().getObjectEnabler(objectId) != null) { + parent.out.printf("Object %d already enabled.%n", objectId); + parent.out.flush(); + } else if (parent.model.getObjectModel(objectId) == null) { + parent.out.printf("Unable to enable Object %d : there no model for this.%n", objectId); + parent.out.flush(); + } else { + ObjectsInitializer objectsInitializer = new ObjectsInitializer(parent.model); + objectsInitializer.setDummyInstancesForObject(objectId); + LwM2mObjectEnabler object = objectsInitializer.create(objectId); + parent.client.getObjectTree().addObjectEnabler(object); + } + } + } + + /** + * A command to delete object enabler. + */ + @Command(name = "delete", description = "Disable a new object", headerHeading = "%n", footer = "") + static class DeleteCommand implements Runnable { + + @Parameters(description = "Id of the LWM2M object to enable") + private Integer objectId; + + @ParentCommand + InteractiveCommands parent; + + @Override + public void run() { + if (objectId == 0 || objectId == 0 || objectId == 3) { + parent.out.printf("Object %d can not be disabled.", objectId); + parent.out.flush(); + } else if (parent.client.getObjectTree().getObjectEnabler(objectId) == null) { + parent.out.printf("Object %d is not enabled.", objectId); + } else { + parent.client.getObjectTree().removeObjectEnabler(objectId); + } + } + } + + /** + * A command to send an update request. + */ + @Command(name = "update", description = "Trigger a registration update.", headerHeading = "%n", footer = "") + static class UpdateCommand implements Runnable { + + @ParentCommand + InteractiveCommands parent; + + @Override + public void run() { + parent.client.triggerRegistrationUpdate(); + } + } + + /** + * A command to move client. + */ + @Command(name = "move", + description = "Simulate client mouvement.", + headerHeading = "%n", + footer = "", + sortOptions = false) + static class MoveCommand implements Runnable { + + @ParentCommand + InteractiveCommands parent; + + @Option(names = { "-w", "north" }, description = "Move to the North") + boolean north; + + @Option(names = { "-a", "east" }, description = "Move to the East") + boolean east; + + @Option(names = { "-s", "south" }, description = "Move to the South") + boolean south; + + @Option(names = { "-d", "west" }, description = "Move to the West") + boolean west; + + @Override + public void run() { + LwM2mObjectEnabler objectEnabler = parent.client.getObjectTree().getObjectEnabler(LwM2mId.LOCATION); + if (objectEnabler != null && objectEnabler instanceof ObjectEnabler) { + LwM2mInstanceEnabler instance = ((ObjectEnabler) objectEnabler).getInstance(0); + if (instance instanceof MyLocation) { + MyLocation location = (MyLocation) instance; + if (north) + location.moveLocation("w"); + if (east) + location.moveLocation("a"); + if (south) + location.moveLocation("s"); + if (west) + location.moveLocation("d"); + } + } + } + } +} \ No newline at end of file diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/TerminalAppender.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/TerminalAppender.java new file mode 100644 index 0000000000..4b2d08aec4 --- /dev/null +++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/interactive/TerminalAppender.java @@ -0,0 +1,46 @@ +package org.eclipse.leshan.client.demo.cli.interactive; + +import java.io.IOException; + +import ch.qos.logback.core.ConsoleAppender; +import jline.console.ConsoleReader; + +/** + * A logback Console appender compatible with a Jline 2 Console reader. + */ +public class TerminalAppender extends ConsoleAppender { + + private ConsoleReader console; + private String prompt; + + @Override + protected void subAppend(E event) { + if (console == null || !console.getTerminal().isAnsiSupported()) + super.subAppend(event); + else { + // stash prompt + String stashed = ""; + try { + stashed = console.getCursorBuffer().copy().toString(); + console.resetPromptLine("", "", -1); + } catch (IOException e) { + e.printStackTrace(); + } + + // Display logs + super.subAppend(event); + + // unstash prompt + try { + console.resetPromptLine(prompt, stashed, -1); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void setConsole(ConsoleReader console) { + this.console = console; + this.prompt = console.getPrompt(); + } +} diff --git a/pom.xml b/pom.xml index 644f65c9e7..fb72c40ddc 100644 --- a/pom.xml +++ b/pom.xml @@ -749,6 +749,11 @@ Contributors: picocli 4.6.1 + + info.picocli + picocli-shell-jline2 + 4.6.1 + commons-cli commons-cli