summaryrefslogtreecommitdiff
path: root/base/src/main/java/bjc/utils/cli/GenericCommandMode.java
diff options
context:
space:
mode:
Diffstat (limited to 'base/src/main/java/bjc/utils/cli/GenericCommandMode.java')
-rw-r--r--base/src/main/java/bjc/utils/cli/GenericCommandMode.java469
1 files changed, 469 insertions, 0 deletions
diff --git a/base/src/main/java/bjc/utils/cli/GenericCommandMode.java b/base/src/main/java/bjc/utils/cli/GenericCommandMode.java
new file mode 100644
index 0000000..8764537
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/GenericCommandMode.java
@@ -0,0 +1,469 @@
+package bjc.utils.cli;
+
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * A general command mode, with a customizable set of commands
+ *
+ * There is a small set of commands which is handled by default. The first is
+ * 'list', which lists all the commands the user can input. The second is
+ * 'alias', which allows the user to bind a new name to a command
+ *
+ * @author ben
+ *
+ */
+public class GenericCommandMode implements CommandMode {
+ /*
+ * Contains the commands this mode handles
+ */
+ private final IMap<String, Command> commandHandlers;
+ private final IMap<String, Command> defaultHandlers;
+
+ /*
+ * Contains help topics without an associated command
+ */
+ private final IMap<String, CommandHelp> helpTopics;
+
+ /*
+ * The action to execute upon encountering an unknown command
+ */
+ private BiConsumer<String, String[]> unknownCommandHandler;
+
+ /*
+ * The functions to use for input/output
+ */
+ private final Consumer<String> errorOutput;
+ private final Consumer<String> normalOutput;
+
+ /*
+ * The name of this command mode, or null if it is unnamed
+ */
+ private String modeName;
+
+ /*
+ * The custom prompt to use, or null if none is specified
+ */
+ private String customPrompt;
+
+ /**
+ * Create a new generic command mode
+ *
+ * @param normalOutput
+ * The function to use for normal output
+ * @param errorOutput
+ * The function to use for error output
+ */
+ public GenericCommandMode(final Consumer<String> normalOutput, final Consumer<String> errorOutput) {
+ if (normalOutput == null)
+ throw new NullPointerException("Normal output source must be non-null");
+ else if (errorOutput == null) throw new NullPointerException("Error output source must be non-null");
+
+ this.normalOutput = normalOutput;
+ this.errorOutput = errorOutput;
+
+ /*
+ * Initialize handler maps so that they sort in alphabetical
+ */
+ /*
+ * order
+ */
+ commandHandlers = new FunctionalMap<>(new TreeMap<>());
+ defaultHandlers = new FunctionalMap<>(new TreeMap<>());
+ helpTopics = new FunctionalMap<>(new TreeMap<>());
+
+ setupDefaultCommands();
+ }
+
+ /**
+ * Add an alias to an existing command
+ *
+ * @param commandName
+ * The name of the command to add an alias for
+ * @param aliasName
+ * The new alias for the command
+ *
+ * @throws IllegalArgumentException
+ * if the specified command doesn't have a bound
+ * handler, or if the alias name already has a bound
+ * value
+ */
+ public void addCommandAlias(final String commandName, final String aliasName) {
+ if (commandName == null)
+ throw new NullPointerException("Command name must not be null");
+ else if (aliasName == null)
+ throw new NullPointerException("Alias name must not be null");
+ else if (!commandHandlers.containsKey(commandName) && !defaultHandlers.containsKey(commandName))
+ throw new IllegalArgumentException("Cannot alias non-existant command '" + commandName + "'");
+ else if (commandHandlers.containsKey(aliasName) || defaultHandlers.containsKey(aliasName))
+ throw new IllegalArgumentException(
+ "Cannot bind alias '" + aliasName + "' to a command with a bound handler");
+ else {
+ Command aliasedCommand;
+
+ if (defaultHandlers.containsKey(commandName)) {
+ aliasedCommand = defaultHandlers.get(commandName).aliased();
+ } else {
+ aliasedCommand = commandHandlers.get(commandName).aliased();
+ }
+
+ commandHandlers.put(aliasName, aliasedCommand);
+ }
+ }
+
+ /**
+ * Add a command to this command mode
+ *
+ * @param command
+ * The name of the command to add
+ * @param handler
+ * The handler to use for the specified command
+ *
+ * @throws IllegalArgumentException
+ * if the specified command already has a handler
+ * registered
+ */
+ public void addCommandHandler(final String command, final Command handler) {
+ if (command == null)
+ throw new NullPointerException("Command must not be null");
+ else if (handler == null)
+ throw new NullPointerException("Handler must not be null");
+ else if (canHandle(command))
+ throw new IllegalArgumentException("Command " + command + " already has a handler registered");
+ else {
+ commandHandlers.put(command, handler);
+ }
+ }
+
+ /**
+ * Add a help topic to this command mode that isn't tied to a command
+ *
+ * @param topicName
+ * The name of the topic
+ * @param topic
+ * The contents of the topic
+ */
+ public void addHelpTopic(final String topicName, final CommandHelp topic) {
+ helpTopics.put(topicName, topic);
+ }
+
+ /*
+ * Default command builders
+ */
+
+ private GenericCommand buildAliasCommand() {
+ final String aliasShortHelp = "alias\tAlias one command to another";
+ final String aliasLongHelp = "Gives a command another name it can be invoked by."
+ + " Invoke with two arguments: the name of the command to alias"
+ + "followed by the name of the alias to give that command.";
+
+ return new GenericCommand((args) -> {
+ doAliasCommands(args);
+
+ return this;
+ }, aliasShortHelp, aliasLongHelp);
+ }
+
+ private GenericCommand buildClearCommands() {
+ final String clearShortHelp = "clear\tClear the screen";
+ final String clearLongHelp = "Clears the screen of all the text on it," + " and prints a new prompt.";
+
+ return new GenericCommand((args) -> {
+ errorOutput.accept("ERROR: This console doesn't support screen clearing");
+
+ return this;
+ }, clearShortHelp, clearLongHelp);
+ }
+
+ private GenericCommand buildExitCommand() {
+ final String exitShortHelp = "exit\tExit the console";
+ final String exitLongHelp = "First prompts the user to make sure they want to"
+ + " exit, then quits if they say they do";
+
+ return new GenericCommand((args) -> {
+ errorOutput.accept("ERROR: This console doesn't support auto-exiting");
+
+ return this;
+ }, exitShortHelp, exitLongHelp);
+ }
+
+ private GenericCommand buildHelpCommand() {
+ final String helpShortHelp = "help\tConsult the help system";
+ final String helpLongHelp = "Consults the internal help system."
+ + " Invoked in two different ways. Invoking with no arguments"
+ + " causes all the topics you can ask for details on to be list,"
+ + " while invoking with the name of a topic will print the entry" + " for that topic";
+
+ return new GenericCommand((args) -> {
+ if (args == null || args.length == 0) {
+ /*
+ * Invoke general help
+ */
+ doHelpSummary();
+ } else {
+ /*
+ * Invoke help for a command
+ */
+ doHelpCommand(args[0]);
+ }
+
+ return this;
+ }, helpShortHelp, helpLongHelp);
+ }
+
+ private GenericCommand buildListCommand() {
+ final String listShortHelp = "list\tList available commands";
+ final String listLongHelp = "Lists all of the commands available in this mode,"
+ + " as well as commands available in any mode";
+
+ return new GenericCommand((args) -> {
+ doListCommands();
+
+ return this;
+ }, listShortHelp, listLongHelp);
+ }
+
+ @Override
+ public boolean canHandle(final String command) {
+ return commandHandlers.containsKey(command) || defaultHandlers.containsKey(command);
+ }
+
+ /*
+ * Implement default commands
+ */
+
+ private void doAliasCommands(final String[] args) {
+ if (args.length != 2) {
+ errorOutput.accept("ERROR: Alias requires two arguments."
+ + " The command name, and the alias for that command");
+ } else {
+ final String commandName = args[0];
+ final String aliasName = args[1];
+
+ if (!canHandle(commandName)) {
+ errorOutput.accept("ERROR: '" + commandName + "' is not a valid command.");
+ } else if (canHandle(aliasName)) {
+ errorOutput.accept("ERROR: Cannot overwrite command '" + aliasName + "'");
+ } else {
+ addCommandAlias(commandName, aliasName);
+ }
+ }
+ }
+
+ private void doHelpCommand(final String commandName) {
+ if (commandHandlers.containsKey(commandName)) {
+ final String desc = commandHandlers.get(commandName).getHelp().getDescription();
+
+ normalOutput.accept("\n" + desc);
+ } else if (defaultHandlers.containsKey(commandName)) {
+ final String desc = defaultHandlers.get(commandName).getHelp().getDescription();
+
+ normalOutput.accept("\n" + desc);
+ } else if (helpTopics.containsKey(commandName)) {
+ normalOutput.accept("\n" + helpTopics.get(commandName).getDescription());
+ } else {
+ errorOutput.accept(
+ "ERROR: I'm sorry, but there is no help available for '" + commandName + "'");
+ }
+ }
+
+ private void doHelpSummary() {
+ normalOutput.accept("Help topics for this command mode are as follows:\n");
+
+ if (commandHandlers.size() > 0) {
+ commandHandlers.forEachValue(command -> {
+ if (!command.isAlias()) {
+ normalOutput.accept("\t" + command.getHelp().getSummary() + "\n");
+ }
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+
+ normalOutput.accept("\nHelp topics available in all command modes are as follows\n");
+ if (defaultHandlers.size() > 0) {
+ defaultHandlers.forEachValue(command -> {
+ if (!command.isAlias()) {
+ normalOutput.accept("\t" + command.getHelp().getSummary() + "\n");
+ }
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+
+ normalOutput.accept("\nHelp topics not associated with a command are as follows\n");
+ if (helpTopics.size() > 0) {
+ helpTopics.forEachValue(topic -> {
+ normalOutput.accept("\t" + topic.getSummary() + "\n");
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+ }
+
+ private void doListCommands() {
+ normalOutput.accept("The available commands for this mode are as follows:\n");
+
+ commandHandlers.keyList().forEach(commandName -> {
+ normalOutput.accept("\t" + commandName);
+ });
+
+ normalOutput.accept("\nThe following commands are available in all modes:\n");
+ defaultHandlers.keyList().forEach(commandName -> {
+ normalOutput.accept("\t" + commandName);
+ });
+
+ normalOutput.accept("\n");
+ }
+
+ @Override
+ public String getCustomPrompt() {
+ if (customPrompt != null) return customPrompt;
+
+ return CommandMode.super.getCustomPrompt();
+ }
+
+ @Override
+ public String getName() {
+ if (modeName != null) return modeName;
+
+ return CommandMode.super.getName();
+ }
+
+ @Override
+ public boolean isCustomPromptEnabled() {
+ return customPrompt != null;
+ }
+
+ @Override
+ public CommandMode process(final String command, final String[] args) {
+ normalOutput.accept("\n");
+
+ if (defaultHandlers.containsKey(command))
+ return defaultHandlers.get(command).getHandler().handle(args);
+ else if (commandHandlers.containsKey(command))
+ return commandHandlers.get(command).getHandler().handle(args);
+ else {
+ if (args != null) {
+ errorOutput.accept("ERROR: Unrecognized command " + command + String.join(" ", args));
+ } else {
+ errorOutput.accept("ERROR: Unrecognized command " + command);
+ }
+
+ if (unknownCommandHandler == null)
+ throw new UnsupportedOperationException("Command " + command + " is invalid.");
+
+ unknownCommandHandler.accept(command, args);
+ }
+
+ return this;
+ }
+
+ /**
+ * Set the custom prompt for this mode
+ *
+ * @param prompt
+ * The custom prompt for this mode, or null to disable
+ * the custom prompt
+ */
+ public void setCustomPrompt(final String prompt) {
+ customPrompt = prompt;
+ }
+
+ /**
+ * Set the name of this mode
+ *
+ * @param name
+ * The desired name of this mode, or null to use the
+ * default name
+ */
+ public void setModeName(final String name) {
+ modeName = name;
+ }
+
+ /**
+ * Set the handler to use for unknown commands
+ *
+ * @param handler
+ * The handler to use for unknown commands, or null to
+ * throw on unknown commands
+ */
+ public void setUnknownCommandHandler(final BiConsumer<String, String[]> handler) {
+ if (handler == null) throw new NullPointerException("Handler must not be null");
+
+ unknownCommandHandler = handler;
+ }
+
+ private void setupDefaultCommands() {
+ defaultHandlers.put("list", buildListCommand());
+ defaultHandlers.put("alias", buildAliasCommand());
+ defaultHandlers.put("help", buildHelpCommand());
+
+ addCommandAlias("help", "man");
+
+ /*
+ * Add commands handled in a upper layer.
+ */
+
+ /*
+ * @TODO figure out a place to put commands that apply across
+ */
+ /*
+ * all
+ */
+ /*
+ * modes, but only apply to a specific application
+ */
+ defaultHandlers.put("clear", buildClearCommands());
+ defaultHandlers.put("exit", buildExitCommand());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("GenericCommandMode [");
+
+ if (commandHandlers != null) {
+ builder.append("commandHandlers=");
+ builder.append(commandHandlers);
+ }
+
+ if (defaultHandlers != null) {
+ builder.append(", ");
+ builder.append("defaultHandlers=");
+ builder.append(defaultHandlers);
+ }
+
+ if (helpTopics != null) {
+ builder.append(", ");
+ builder.append("helpTopics=");
+ builder.append(helpTopics);
+ }
+
+ if (modeName != null) {
+ builder.append(", ");
+ builder.append("modeName=");
+ builder.append(modeName);
+ }
+
+ if (customPrompt != null) {
+ builder.append(", ");
+ builder.append("customPrompt=");
+ builder.append(customPrompt);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+
+}