Skip to content

Commit

Permalink
Implement the InteractiveCLI using Jline2 and picocli
Browse files Browse the repository at this point in the history
  • Loading branch information
sbernard31 committed Aug 3, 2021
1 parent a5ca5c7 commit 863ff21
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 93 deletions.
4 changes: 2 additions & 2 deletions leshan-client-demo/logback-config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ Contributors:
Sierra Wireless - initial API and implementation
-->
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<appender name="TERMINAL" class="org.eclipse.leshan.client.demo.cli.interactive.TerminalAppender">
<encoder>
<pattern>%d %p %C{0} - %m%n</pattern>
</encoder>
</appender>

<root level="WARN">
<appender-ref ref="STDOUT" />
<appender-ref ref="TERMINAL" />
</root>

<logger name="org.eclipse.leshan" level="INFO" />
Expand Down
7 changes: 4 additions & 3 deletions leshan-client-demo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ Contributors:
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency>

<!-- runtime dependencies -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli-shell-jline2</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>runtime</scope>
</dependency>

<!-- test dependencies -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <objectId> : to enable a new object.");
commandsHelp.append(System.lineSeparator());
commandsHelp.append(" - delete <objectId> : 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<Character> 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, <objectid> must be an integer : create <objectId>");
}
} 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, <objectid> must be an integer : delete <objectId>");
}
} 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
}
}
Loading

0 comments on commit 863ff21

Please sign in to comment.