From efcb155b3d7ec2d7f2cca51cd6cddcd1f7e2b668 Mon Sep 17 00:00:00 2001 From: Marc Baloup Date: Sun, 23 Apr 2023 00:59:39 +0200 Subject: [PATCH] Improved pandalib-cli - Custom class to represent command sender - Abstract class to use as a base for CLI application main class - /stop and /admin command that are common to every CLI apps --- .../fr/pandacube/lib/cli/CLIApplication.java | 103 +++++++ .../lib/cli/commands/CLIBrigadierCommand.java | 20 +- .../cli/commands/CLIBrigadierDispatcher.java | 23 +- .../lib/cli/commands/CLICommandSender.java | 45 +++ .../cli/commands/CLIConsoleCommandSender.java | 37 +++ .../lib/cli/commands/CommandAdmin.java | 273 ++++++++++++++++++ .../lib/cli/commands/CommandStop.java | 25 ++ 7 files changed, 503 insertions(+), 23 deletions(-) create mode 100644 pandalib-cli/src/main/java/fr/pandacube/lib/cli/CLIApplication.java create mode 100644 pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLICommandSender.java create mode 100644 pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIConsoleCommandSender.java create mode 100644 pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CommandAdmin.java create mode 100644 pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CommandStop.java diff --git a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/CLIApplication.java b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/CLIApplication.java new file mode 100644 index 0000000..a63ce29 --- /dev/null +++ b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/CLIApplication.java @@ -0,0 +1,103 @@ +package fr.pandacube.lib.cli; + +import fr.pandacube.lib.cli.commands.CommandAdmin; +import fr.pandacube.lib.cli.commands.CommandStop; +import fr.pandacube.lib.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * Main class of a CLI application. + */ +public abstract class CLIApplication { + + private static CLIApplication instance; + + public static CLIApplication getInstance() { + return instance; + } + + + + + + + public final CLI cli; + + protected CLIApplication() { + instance = this; + CLI tmpCLI = null; + try { + tmpCLI = new CLI(); + Log.setLogger(tmpCLI.getLogger()); + } catch (Throwable t) { + System.err.println("Unable to start application " + getName() + " version " + getClass().getPackage().getImplementationVersion()); + t.printStackTrace(); + System.exit(1); + } + cli = tmpCLI; + + try { + Log.info("Starting " + getName() + " version " + getClass().getPackage().getImplementationVersion()); + + start(); + + new CommandAdmin(); + new CommandStop(); + + Runtime.getRuntime().addShutdownHook(new Thread(this::end)); + + cli.start(); // actually starts the CLI thread + + Log.info("Application started."); + } catch (Throwable t) { + Log.severe("Unable to start application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t); + } + } + + public Logger getLogger() { + return cli.getLogger(); + } + + + private final Object stopLock = new Object(); + private final AtomicBoolean stopping = new AtomicBoolean(false); + + public final void stop() { + synchronized (stopLock) { + synchronized (stopping) { + if (stopping.get()) + return; + stopping.set(true); + } + Log.info("Stopping " + getName() + " version " + getClass().getPackage().getImplementationVersion()); + try { + end(); + } catch (Throwable t) { + Log.severe("Error stopping application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t); + } finally { + Log.info("Bye bye."); + System.exit(0); + } + } + + } + + public boolean isStopping() { + return stopping.get(); + } + + + + + public abstract String getName(); + + protected abstract void start() throws Exception; + + public abstract void reload(); + + protected abstract void end(); + + +} diff --git a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIBrigadierCommand.java b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIBrigadierCommand.java index c3041a7..a715fd9 100644 --- a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIBrigadierCommand.java +++ b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIBrigadierCommand.java @@ -12,13 +12,13 @@ import java.util.function.Predicate; /** * Abstract class that holds the logic of a specific command to be registered in {@link CLIBrigadierDispatcher}. */ -public abstract class CLIBrigadierCommand extends BrigadierCommand { +public abstract class CLIBrigadierCommand extends BrigadierCommand { /** * Instanciate this command instance. */ public CLIBrigadierCommand() { - LiteralCommandNode commandNode = buildCommand().build(); + LiteralCommandNode commandNode = buildCommand().build(); postBuildCommand(commandNode); String[] aliases = getAliases(); if (aliases == null) @@ -37,7 +37,7 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand { } } - protected abstract LiteralArgumentBuilder buildCommand(); + protected abstract LiteralArgumentBuilder buildCommand(); protected String[] getAliases() { return new String[0]; @@ -47,16 +47,16 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand { - public boolean isPlayer(Object sender) { - return false; + public boolean isPlayer(CLICommandSender sender) { + return sender.isPlayer(); } - public boolean isConsole(Object sender) { - return true; + public boolean isConsole(CLICommandSender sender) { + return sender.isConsole(); } - public Predicate hasPermission(String permission) { - return sender -> true; + public Predicate hasPermission(String permission) { + return sender -> sender.hasPermission(permission); } @@ -68,7 +68,7 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand { * @param suggestions the suggestions to wrap. * @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}. */ - protected SuggestionProvider wrapSuggestions(SuggestionsSupplier suggestions) { + protected SuggestionProvider wrapSuggestions(SuggestionsSupplier suggestions) { return wrapSuggestions(suggestions, Function.identity()); } diff --git a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIBrigadierDispatcher.java b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIBrigadierDispatcher.java index 78acb49..6021eca 100644 --- a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIBrigadierDispatcher.java +++ b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIBrigadierDispatcher.java @@ -1,20 +1,17 @@ package fr.pandacube.lib.cli.commands; -import java.util.List; - import com.mojang.brigadier.suggestion.Suggestion; import com.mojang.brigadier.suggestion.Suggestions; - -import fr.pandacube.lib.chat.Chat; import fr.pandacube.lib.commands.BrigadierDispatcher; -import fr.pandacube.lib.util.Log; import jline.console.completer.Completer; import net.kyori.adventure.text.ComponentLike; +import java.util.List; + /** * Implementation of {@link BrigadierDispatcher} that integrates the commands into the JLine CLI interface. */ -public class CLIBrigadierDispatcher extends BrigadierDispatcher implements Completer { +public class CLIBrigadierDispatcher extends BrigadierDispatcher implements Completer { /** * The instance of {@link CLIBrigadierDispatcher}. @@ -22,16 +19,16 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher implemen public static final CLIBrigadierDispatcher instance = new CLIBrigadierDispatcher(); - private static final Object sender = new Object(); + private static final CLICommandSender CLI_CONSOLE_COMMAND_SENDER = new CLIConsoleCommandSender(); /** - * Executes the provided command. + * Executes the provided command as the console. * @param commandWithoutSlash the command, without the eventual slash at the begining. * @return the value returned by the executed command. */ public int execute(String commandWithoutSlash) { - return execute(sender, commandWithoutSlash); + return execute(CLI_CONSOLE_COMMAND_SENDER, commandWithoutSlash); } @@ -50,17 +47,17 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher implemen } /** - * Gets the suggestions for the currently being typed command. + * Gets the suggestions for the currently being typed command, as the console. * @param buffer the command that is being typed. * @return the suggestions for the currently being typed command. */ public Suggestions getSuggestions(String buffer) { - return getSuggestions(sender, buffer); + return getSuggestions(CLI_CONSOLE_COMMAND_SENDER, buffer); } @Override - protected void sendSenderMessage(Object sender, ComponentLike message) { - Log.info(Chat.chatComponent(message).getLegacyText()); + protected void sendSenderMessage(CLICommandSender sender, ComponentLike message) { + sender.sendMessage(message); } } diff --git a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLICommandSender.java b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLICommandSender.java new file mode 100644 index 0000000..d6d0a82 --- /dev/null +++ b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLICommandSender.java @@ -0,0 +1,45 @@ +package fr.pandacube.lib.cli.commands; + +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.audience.MessageType; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; + +/** + * A command sender. + */ +public interface CLICommandSender extends Audience { + /** + * Gets the name of the sender. + * @return The name of the sender. + */ + String getName(); + + /** + * Tells if the sender is a player. + * @return true if the sender is a player, false otherwise. + */ + boolean isPlayer(); + + /** + * Tells if the sender is on the console. + * @return true if the sender is on the console, false otherwise. + */ + boolean isConsole(); + + /** + * Tells if the sender has the specified permission. + * @param permission the permission to test on the sender. + * @return true if the sender has the specified permission. + */ + boolean hasPermission(String permission); + + /** + * Sends the provided message to the sender. + * @param message the message to send. + */ + void sendMessage(String message); + + @Override // force implementation of super-interface default method + void sendMessage(Identity source, Component message, MessageType type); +} diff --git a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIConsoleCommandSender.java b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIConsoleCommandSender.java new file mode 100644 index 0000000..2506080 --- /dev/null +++ b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CLIConsoleCommandSender.java @@ -0,0 +1,37 @@ +package fr.pandacube.lib.cli.commands; + +import fr.pandacube.lib.chat.Chat; +import fr.pandacube.lib.util.Log; +import net.kyori.adventure.audience.MessageType; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; + +/** + * The console command sender. + */ +public class CLIConsoleCommandSender implements CLICommandSender { + public String getName() { + return "Console"; + } + + public boolean isPlayer() { + return false; + } + + public boolean isConsole() { + return true; + } + + public boolean hasPermission(String permission) { + return true; + } + + public void sendMessage(String message) { + Log.info(message); + } + + @Override + public void sendMessage(Identity source, Component message, MessageType type) { + sendMessage(Chat.chatComponent(message).getLegacyText()); + } +} diff --git a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CommandAdmin.java b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CommandAdmin.java new file mode 100644 index 0000000..ef9a3c7 --- /dev/null +++ b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CommandAdmin.java @@ -0,0 +1,273 @@ +package fr.pandacube.lib.cli.commands; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.LongArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; +import fr.pandacube.lib.chat.Chat; +import fr.pandacube.lib.chat.Chat.FormatableChat; +import fr.pandacube.lib.chat.ChatTreeNode; +import fr.pandacube.lib.cli.CLIApplication; +import fr.pandacube.lib.util.Log; +import net.md_5.bungee.api.chat.BaseComponent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static fr.pandacube.lib.chat.ChatStatic.chat; +import static fr.pandacube.lib.chat.ChatStatic.failureText; +import static fr.pandacube.lib.chat.ChatStatic.successText; +import static fr.pandacube.lib.chat.ChatStatic.text; + +public class CommandAdmin extends CLIBrigadierCommand { + + @Override + protected LiteralArgumentBuilder buildCommand() { + return literal("admin") + .executes(this::version) + .then(literal("version") + .executes(this::version) + ) + .then(literal("reload") + .executes(this::reload) + ) + .then(literal("debug") + .executes(this::debug) + ) + .then(literal("commandstruct") + .executes(this::commandStruct) + .then(argument("path", StringArgumentType.greedyString()) + .executes(this::commandStruct) + ) + ); + } + + + private int version(CommandContext context) { + Log.info(chat() + .console(context.getSource().isConsole()) + .infoColor() + .thenCenterText(text(CLIApplication.getInstance().getName())) + .thenNewLine() + .thenText("- Implem. version: ") + .thenData(CLIApplication.getInstance().getClass().getPackage().getImplementationVersion()) + .thenNewLine() + .thenText("- Spec. version: ") + .thenData(CLIApplication.getInstance().getClass().getPackage().getSpecificationVersion()) + .getLegacyText()); + return 1; + } + + + + private int reload(CommandContext context) { + CLIApplication.getInstance().reload(); + return 1; + } + + private int debug(CommandContext context) { + Log.setDebug(!Log.isDebugEnabled()); + Log.info(successText("Mode débug " + + (Log.isDebugEnabled() ? "" : "dés") + "activé").getLegacyText()); + return 1; + } + + private int commandStruct(CommandContext context) { + CLICommandSender sender = context.getSource(); + String[] tokens = tryGetArgument(context, "path", String.class, s -> s.split(" "), new String[0]); + + CommandNode node = CLIBrigadierDispatcher.instance.getDispatcher().findNode(Arrays.asList(tokens)); + + if (node == null) { + Log.severe(failureText("La commande spécifiée n’a pas été trouvée.").getLegacyText()); + return 0; + } + + Set> scannedNodes = new HashSet<>(); + DisplayCommandNode displayNode = new DisplayCommandNode(); + + // find parent nodes of scanned node to avoid displaying them after redirection and stuff + for (int i = 1; i < tokens.length; i++) { + CommandNode ignoredNode = CLIBrigadierDispatcher.instance.getDispatcher().findNode(Arrays.asList(Arrays.copyOf(tokens, i))); + if (ignoredNode != null) { + displayNode.addInline(ignoredNode); + scannedNodes.add(ignoredNode); + } + } + + buildDisplayCommandTree(displayNode, scannedNodes, node); + + ChatTreeNode displayTreeNode = buildDisplayTree(displayNode, sender); + for (Chat comp : displayTreeNode.render(true)) + Log.info(comp.getLegacyText()); + return 1; + } + + + + + + + + + private void buildDisplayCommandTree(DisplayCommandNode displayNode, Set> scannedNodes, CommandNode node) { + displayNode.addInline(node); + + scannedNodes.add(node); + + if (node.getRedirect() != null) { + if (scannedNodes.contains(node.getRedirect()) || node.getRedirect() instanceof RootCommandNode) { + displayNode.addInline(node.getRedirect()); + } + else { + buildDisplayCommandTree(displayNode, scannedNodes, node.getRedirect()); + } + } + else if (node.getChildren().size() == 1) { + buildDisplayCommandTree(displayNode, scannedNodes, node.getChildren().iterator().next()); + } + else if (node.getChildren().size() >= 2) { + for (CommandNode child : node.getChildren()) { + DisplayCommandNode dNode = new DisplayCommandNode(); + buildDisplayCommandTree(dNode, scannedNodes, child); + displayNode.addChild(dNode); + } + } + } + + + + + private ChatTreeNode buildDisplayTree(DisplayCommandNode displayNode, CLICommandSender sender) { + Chat d = chat().then(displayCurrentNode(displayNode.nodes.get(0), false, sender)); + + CommandNode prevNode = displayNode.nodes.get(0); + for (int i = 1; i < displayNode.nodes.size(); i++) { + CommandNode currNode = displayNode.nodes.get(i); + if (currNode.equals(prevNode.getRedirect())) { + d.then(text(" → ") + .hover("Redirects to path: " + CLIBrigadierDispatcher.instance.getDispatcher().getPath(currNode)) + ); + d.then(displayCurrentNode(currNode, true, sender)); + } + else { + d.thenText(" "); + d.then(displayCurrentNode(currNode, false, sender)); + } + prevNode = currNode; + } + + + ChatTreeNode dispTree = new ChatTreeNode(d); + + for (DisplayCommandNode child : displayNode.children) { + dispTree.addChild(buildDisplayTree(child, sender)); + } + + return dispTree; + + } + + + + + + private BaseComponent displayCurrentNode(CommandNode node, boolean redirectTarget, CLICommandSender sender) { + if (node == null) + throw new IllegalArgumentException("node must not be null"); + FormatableChat d; + if (node instanceof RootCommandNode) { + d = text("(root)").italic() + .hover("Root command node"); + } + else if (node instanceof ArgumentCommandNode) { + ArgumentType type = ((ArgumentCommandNode) node).getType(); + String typeStr = type.getClass().getSimpleName(); + if (type instanceof IntegerArgumentType + || type instanceof LongArgumentType + || type instanceof FloatArgumentType + || type instanceof DoubleArgumentType) { + typeStr = type.toString(); + } + else if (type instanceof BoolArgumentType) { + typeStr = "bool()"; + } + else if (type instanceof StringArgumentType) { + typeStr = "string(" + ((StringArgumentType) type).getType().name().toLowerCase() + ")"; + } + String t = "<" + node.getName() + ">"; + String h = "Argument command node" + + "\nType: " + typeStr; + + if (node.getCommand() != null) { + t += "®"; + h += "\nThis node has a command"; + } + + d = text(t); + + if (!node.canUse(sender)) { + d.gray(); + h += "\nPermission not granted for you"; + } + + d.hover(h); + } + else if (node instanceof LiteralCommandNode) { + String t = node.getName(); + String h = "Literal command node"; + + if (node.getCommand() != null) { + t += "®"; + h += "\nThis node has a command"; + } + + d = text(t); + + if (!node.canUse(sender)) { + d.gray(); + h += "\nPermission not granted for you"; + } + + d.hover(h); + } + else { + throw new IllegalArgumentException("Unknown command node type: " + node.getClass()); + } + + if (redirectTarget) + d.gray(); + return d.get(); + + } + + + + + private static class DisplayCommandNode { + List> nodes = new ArrayList<>(); + List children = new ArrayList<>(); + + void addInline(CommandNode node) { + nodes.add(node); + } + + void addChild(DisplayCommandNode child) { + children.add(child); + } + } + + +} diff --git a/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CommandStop.java b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CommandStop.java new file mode 100644 index 0000000..ef440c5 --- /dev/null +++ b/pandalib-cli/src/main/java/fr/pandacube/lib/cli/commands/CommandStop.java @@ -0,0 +1,25 @@ +package fr.pandacube.lib.cli.commands; + + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import fr.pandacube.lib.cli.CLIApplication; + +/** + * /stop (/end) command. + */ +public class CommandStop extends CLIBrigadierCommand { + + @Override + protected LiteralArgumentBuilder buildCommand() { + return literal("stop") + .executes(context -> { + CLIApplication.getInstance().stop(); + return 1; + }); + } + + @Override + protected String[] getAliases() { + return new String[] { "end" }; + } +}