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
This commit is contained in:
Marc Baloup 2023-04-23 00:59:39 +02:00
parent d5c9876734
commit efcb155b3d
7 changed files with 503 additions and 23 deletions

View File

@ -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();
}

View File

@ -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}. * Abstract class that holds the logic of a specific command to be registered in {@link CLIBrigadierDispatcher}.
*/ */
public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> { public abstract class CLIBrigadierCommand extends BrigadierCommand<CLICommandSender> {
/** /**
* Instanciate this command instance. * Instanciate this command instance.
*/ */
public CLIBrigadierCommand() { public CLIBrigadierCommand() {
LiteralCommandNode<Object> commandNode = buildCommand().build(); LiteralCommandNode<CLICommandSender> commandNode = buildCommand().build();
postBuildCommand(commandNode); postBuildCommand(commandNode);
String[] aliases = getAliases(); String[] aliases = getAliases();
if (aliases == null) if (aliases == null)
@ -37,7 +37,7 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
} }
} }
protected abstract LiteralArgumentBuilder<Object> buildCommand(); protected abstract LiteralArgumentBuilder<CLICommandSender> buildCommand();
protected String[] getAliases() { protected String[] getAliases() {
return new String[0]; return new String[0];
@ -47,16 +47,16 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
public boolean isPlayer(Object sender) { public boolean isPlayer(CLICommandSender sender) {
return false; return sender.isPlayer();
} }
public boolean isConsole(Object sender) { public boolean isConsole(CLICommandSender sender) {
return true; return sender.isConsole();
} }
public Predicate<Object> hasPermission(String permission) { public Predicate<CLICommandSender> hasPermission(String permission) {
return sender -> true; return sender -> sender.hasPermission(permission);
} }
@ -68,7 +68,7 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
* @param suggestions the suggestions to wrap. * @param suggestions the suggestions to wrap.
* @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}. * @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}.
*/ */
protected SuggestionProvider<Object> wrapSuggestions(SuggestionsSupplier<Object> suggestions) { protected SuggestionProvider<CLICommandSender> wrapSuggestions(SuggestionsSupplier<CLICommandSender> suggestions) {
return wrapSuggestions(suggestions, Function.identity()); return wrapSuggestions(suggestions, Function.identity());
} }

View File

@ -1,20 +1,17 @@
package fr.pandacube.lib.cli.commands; package fr.pandacube.lib.cli.commands;
import java.util.List;
import com.mojang.brigadier.suggestion.Suggestion; import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.Suggestions;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.commands.BrigadierDispatcher; import fr.pandacube.lib.commands.BrigadierDispatcher;
import fr.pandacube.lib.util.Log;
import jline.console.completer.Completer; import jline.console.completer.Completer;
import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.ComponentLike;
import java.util.List;
/** /**
* Implementation of {@link BrigadierDispatcher} that integrates the commands into the JLine CLI interface. * Implementation of {@link BrigadierDispatcher} that integrates the commands into the JLine CLI interface.
*/ */
public class CLIBrigadierDispatcher extends BrigadierDispatcher<Object> implements Completer { public class CLIBrigadierDispatcher extends BrigadierDispatcher<CLICommandSender> implements Completer {
/** /**
* The instance of {@link CLIBrigadierDispatcher}. * The instance of {@link CLIBrigadierDispatcher}.
@ -22,16 +19,16 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<Object> implemen
public static final CLIBrigadierDispatcher instance = new CLIBrigadierDispatcher(); 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. * @param commandWithoutSlash the command, without the eventual slash at the begining.
* @return the value returned by the executed command. * @return the value returned by the executed command.
*/ */
public int execute(String commandWithoutSlash) { 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<Object> 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. * @param buffer the command that is being typed.
* @return the suggestions for the currently being typed command. * @return the suggestions for the currently being typed command.
*/ */
public Suggestions getSuggestions(String buffer) { public Suggestions getSuggestions(String buffer) {
return getSuggestions(sender, buffer); return getSuggestions(CLI_CONSOLE_COMMAND_SENDER, buffer);
} }
@Override @Override
protected void sendSenderMessage(Object sender, ComponentLike message) { protected void sendSenderMessage(CLICommandSender sender, ComponentLike message) {
Log.info(Chat.chatComponent(message).getLegacyText()); sender.sendMessage(message);
} }
} }

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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<CLICommandSender> 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<CLICommandSender> 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<CLICommandSender> context) {
CLIApplication.getInstance().reload();
return 1;
}
private int debug(CommandContext<CLICommandSender> context) {
Log.setDebug(!Log.isDebugEnabled());
Log.info(successText("Mode débug "
+ (Log.isDebugEnabled() ? "" : "dés") + "activé").getLegacyText());
return 1;
}
private int commandStruct(CommandContext<CLICommandSender> context) {
CLICommandSender sender = context.getSource();
String[] tokens = tryGetArgument(context, "path", String.class, s -> s.split(" "), new String[0]);
CommandNode<CLICommandSender> node = CLIBrigadierDispatcher.instance.getDispatcher().findNode(Arrays.asList(tokens));
if (node == null) {
Log.severe(failureText("La commande spécifiée na pas été trouvée.").getLegacyText());
return 0;
}
Set<CommandNode<CLICommandSender>> 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<CLICommandSender> 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<CommandNode<CLICommandSender>> scannedNodes, CommandNode<CLICommandSender> 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<CLICommandSender> 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<CLICommandSender> prevNode = displayNode.nodes.get(0);
for (int i = 1; i < displayNode.nodes.size(); i++) {
CommandNode<CLICommandSender> 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<CLICommandSender> 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<CommandNode<CLICommandSender>> nodes = new ArrayList<>();
List<DisplayCommandNode> children = new ArrayList<>();
void addInline(CommandNode<CLICommandSender> node) {
nodes.add(node);
}
void addChild(DisplayCommandNode child) {
children.add(child);
}
}
}

View File

@ -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<CLICommandSender> buildCommand() {
return literal("stop")
.executes(context -> {
CLIApplication.getInstance().stop();
return 1;
});
}
@Override
protected String[] getAliases() {
return new String[] { "end" };
}
}