From f33a93a999932e1a19a56e306ba489914123706f Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 7 Jan 2022 10:40:28 +0100 Subject: [PATCH] Reintegrate CLI into CMS --- dep/org.argeo.dep.cms.base/pom.xml | 4 + dep/org.argeo.dep.cms.ext/pom.xml | 10 +- .../argeo/cms/cli/CommandArgsException.java | 32 ++++ .../cms/cli/CommandRuntimeException.java | 35 +++++ .../src/org/argeo/cms/cli/CommandsCli.java | 131 ++++++++++++++++ .../org/argeo/cms/cli/DescribedCommand.java | 55 +++++++ .../src/org/argeo/cms/cli/HelpCommand.java | 143 ++++++++++++++++++ .../src/org/argeo/cms/cli/package-info.java | 2 + 8 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/package-info.java diff --git a/dep/org.argeo.dep.cms.base/pom.xml b/dep/org.argeo.dep.cms.base/pom.xml index 0ecb0220a..a46e8ec30 100644 --- a/dep/org.argeo.dep.cms.base/pom.xml +++ b/dep/org.argeo.dep.cms.base/pom.xml @@ -112,6 +112,10 @@ org.argeo.tp.apache.commons org.apache.commons.io + + org.argeo.tp.apache.commons + org.apache.commons.cli + org.argeo.tp.apache.commons org.apache.commons.codec diff --git a/dep/org.argeo.dep.cms.ext/pom.xml b/dep/org.argeo.dep.cms.ext/pom.xml index af26bb8c7..ea6d99541 100644 --- a/dep/org.argeo.dep.cms.ext/pom.xml +++ b/dep/org.argeo.dep.cms.ext/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 org.argeo.commons @@ -75,6 +77,12 @@ org.apache.xerces + + + org.argeo.tp.apache.commons + org.apache.commons.exec + + diff --git a/org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java b/org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java new file mode 100644 index 000000000..1f6d56bb1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java @@ -0,0 +1,32 @@ +package org.argeo.cms.cli; + +public class CommandArgsException extends IllegalArgumentException { + private static final long serialVersionUID = -7271050747105253935L; + private String commandName; + private volatile CommandsCli commandsCli; + + public CommandArgsException(Exception cause) { + super(cause.getMessage(), cause); + } + + public CommandArgsException(String message) { + super(message); + } + + public String getCommandName() { + return commandName; + } + + public void setCommandName(String commandName) { + this.commandName = commandName; + } + + public CommandsCli getCommandsCli() { + return commandsCli; + } + + public void setCommandsCli(CommandsCli commandsCli) { + this.commandsCli = commandsCli; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java b/org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java new file mode 100644 index 000000000..ef27c1fc8 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java @@ -0,0 +1,35 @@ +package org.argeo.cms.cli; + +import java.util.List; + +/** {@link RuntimeException} referring during a command run. */ +public class CommandRuntimeException extends RuntimeException { + private static final long serialVersionUID = 5595999301269377128L; + + private final DescribedCommand command; + private final List arguments; + + public CommandRuntimeException(Throwable e, DescribedCommand command, List arguments) { + this(null, e, command, arguments); + } + + public CommandRuntimeException(String message, DescribedCommand command, List arguments) { + this(message, null, command, arguments); + } + + public CommandRuntimeException(String message, Throwable e, DescribedCommand command, List arguments) { + super(message == null ? "(" + command.getClass().getName() + " " + arguments.toString() + ")" + : message + " (" + command.getClass().getName() + " " + arguments.toString() + ")", e); + this.command = command; + this.arguments = arguments; + } + + public DescribedCommand getCommand() { + return command; + } + + public List getArguments() { + return arguments; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java b/org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java new file mode 100644 index 000000000..d45ea33a0 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java @@ -0,0 +1,131 @@ +package org.argeo.cms.cli; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Function; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** Base class for a CLI managing sub commands. */ +public abstract class CommandsCli implements DescribedCommand { + public final static String HELP = "help"; + + private final String commandName; + private Map, ?>> commands = new TreeMap<>(); + + protected final Options options = new Options(); + + public CommandsCli(String commandName) { + this.commandName = commandName; + } + + @Override + public Object apply(List args) { + String cmd = null; + List newArgs = new ArrayList<>(); + try { + CommandLineParser clParser = new DefaultParser(); + CommandLine commonCl = clParser.parse(getOptions(), args.toArray(new String[args.size()]), true); + List leftOvers = commonCl.getArgList(); + for (String arg : leftOvers) { + if (!arg.startsWith("-") && cmd == null) { + cmd = arg; + } else { + newArgs.add(arg); + } + } + } catch (ParseException e) { + CommandArgsException cae = new CommandArgsException(e); + throw cae; + } + + Function, ?> function = cmd != null ? getCommand(cmd) : getDefaultCommand(); + if (function == null) + throw new IllegalArgumentException("Uknown command " + cmd); + try { + return function.apply(newArgs).toString(); + } catch (CommandArgsException e) { + if (e.getCommandName() == null) { + e.setCommandName(cmd); + e.setCommandsCli(this); + } + throw e; + } catch (IllegalArgumentException e) { + CommandArgsException cae = new CommandArgsException(e); + cae.setCommandName(cmd); + throw cae; + } + } + + @Override + public Options getOptions() { + return options; + } + + protected void addCommand(String cmd, Function, ?> function) { + commands.put(cmd, function); + + } + + @Override + public String getUsage() { + return "[command]"; + } + + protected void addCommandsCli(CommandsCli commandsCli) { + addCommand(commandsCli.getCommandName(), commandsCli); + commandsCli.addCommand(HELP, new HelpCommand(this, commandsCli)); + } + + public String getCommandName() { + return commandName; + } + + public Set getSubCommands() { + return commands.keySet(); + } + + public Function, ?> getCommand(String command) { + return commands.get(command); + } + + public HelpCommand getHelpCommand() { + return (HelpCommand) getCommand(HELP); + } + + public Function, String> getDefaultCommand() { + return getHelpCommand(); + } + + /** In order to implement quickly a main method. */ + public static void mainImpl(CommandsCli cli, String[] args) { + try { + cli.addCommand(CommandsCli.HELP, new HelpCommand(null, cli)); + Object output = cli.apply(Arrays.asList(args)); + System.out.println(output); + System.exit(0); + } catch (CommandArgsException e) { + System.err.println("Wrong arguments " + Arrays.toString(args) + ": " + e.getMessage()); + if (e.getCommandName() != null) { + StringWriter out = new StringWriter(); + HelpCommand.printHelp(e.getCommandsCli(), e.getCommandName(), out); + System.err.println(out.toString()); + } else { + e.printStackTrace(); + } + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java b/org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java new file mode 100644 index 000000000..cdfe1300d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java @@ -0,0 +1,55 @@ +package org.argeo.cms.cli; + +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** A command that can be described. */ +public interface DescribedCommand extends Function, T> { + default Options getOptions() { + return new Options(); + } + + String getDescription(); + + default String getUsage() { + return null; + } + + default String getExamples() { + return null; + } + + default CommandLine toCommandLine(List args) { + try { + DefaultParser parser = new DefaultParser(); + return parser.parse(getOptions(), args.toArray(new String[args.size()])); + } catch (ParseException e) { + throw new CommandArgsException(e); + } + } + + /** In order to implement quickly a main method. */ + public static void mainImpl(DescribedCommand command, String[] args) { + try { + Object output = command.apply(Arrays.asList(args)); + System.out.println(output); + System.exit(0); + } catch (IllegalArgumentException e) { + StringWriter out = new StringWriter(); + HelpCommand.printHelp(command, out); + System.err.println(out.toString()); + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java b/org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java new file mode 100644 index 000000000..ab899f437 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java @@ -0,0 +1,143 @@ +package org.argeo.cms.cli; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.function.Function; + +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; + +/** A special command that can describe {@link DescribedCommand}. */ +public class HelpCommand implements DescribedCommand { + private CommandsCli commandsCli; + private CommandsCli parentCommandsCli; + + // Help formatting + private static int helpWidth = 80; + private static int helpLeftPad = 4; + private static int helpDescPad = 20; + + public HelpCommand(CommandsCli parentCommandsCli, CommandsCli commandsCli) { + super(); + this.parentCommandsCli = parentCommandsCli; + this.commandsCli = commandsCli; + } + + @Override + public String apply(List args) { + StringWriter out = new StringWriter(); + + if (args.size() == 0) {// overview + printHelp(commandsCli, out); + } else { + String cmd = args.get(0); + Function, ?> function = commandsCli.getCommand(cmd); + if (function == null) + return "Command " + cmd + " not found."; + Options options; + String examples; + DescribedCommand command = null; + if (function instanceof DescribedCommand) { + command = (DescribedCommand) function; + options = command.getOptions(); + examples = command.getExamples(); + } else { + options = new Options(); + examples = null; + } + String description = getShortDescription(function); + String commandCall = getCommandUsage(cmd, command); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, commandCall, description, options, helpLeftPad, + helpDescPad, examples, false); + } + return out.toString(); + } + + private static String getShortDescription(Function, ?> function) { + if (function instanceof DescribedCommand) { + return ((DescribedCommand) function).getDescription(); + } else { + return function.toString(); + } + } + + public String getCommandUsage(String cmd, DescribedCommand command) { + String commandCall = getCommandCall(commandsCli) + " " + cmd; + assert command != null; + if (command != null && command.getUsage() != null) { + commandCall = commandCall + " " + command.getUsage(); + } + return commandCall; + } + + @Override + public String getDescription() { + return "Shows this help or describes a command"; + } + + @Override + public String getUsage() { + return "[command]"; + } + + public CommandsCli getParentCommandsCli() { + return parentCommandsCli; + } + + protected String getCommandCall(CommandsCli commandsCli) { + HelpCommand hc = commandsCli.getHelpCommand(); + if (hc.getParentCommandsCli() != null) { + return getCommandCall(hc.getParentCommandsCli()) + " " + commandsCli.getCommandName(); + } else { + return commandsCli.getCommandName(); + } + } + + public static void printHelp(DescribedCommand command, StringWriter out) { + String usage = "java " + command.getClass().getName() + + (command.getUsage() != null ? " " + command.getUsage() : ""); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, usage, command.getDescription(), command.getOptions(), + helpLeftPad, helpDescPad, command.getExamples(), false); + + } + + public static void printHelp(CommandsCli commandsCli, String commandName, StringWriter out) { + DescribedCommand command = (DescribedCommand) commandsCli.getCommand(commandName); + String usage = commandsCli.getHelpCommand().getCommandUsage(commandName, command); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, usage, command.getDescription(), command.getOptions(), + helpLeftPad, helpDescPad, command.getExamples(), false); + + } + + public static void printHelp(CommandsCli commandsCli, StringWriter out) { + out.append(commandsCli.getDescription()).append('\n'); + String leftPad = spaces(helpLeftPad); + for (String cmd : commandsCli.getSubCommands()) { + Function, ?> function = commandsCli.getCommand(cmd); + assert function != null; + out.append(leftPad); + out.append(cmd); + // TODO deal with long commands + out.append(spaces(helpDescPad - cmd.length())); + out.append(getShortDescription(function)); + out.append('\n'); + } + } + + private static String spaces(int count) { + // Java 11 + // return " ".repeat(count); + if (count <= 0) + return ""; + else { + StringBuilder sb = new StringBuilder(count); + for (int i = 0; i < count; i++) + sb.append(' '); + return sb.toString(); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/package-info.java b/org.argeo.cms/src/org/argeo/cms/cli/package-info.java new file mode 100644 index 000000000..59feed1d1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/package-info.java @@ -0,0 +1,2 @@ +/** Command line API. */ +package org.argeo.cms.cli; \ No newline at end of file -- 2.30.2