diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/ArgumentType.java b/src/main/java/fr/zcraft/quartzlib/components/commands/ArgumentType.java new file mode 100644 index 00000000..636ef090 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/ArgumentType.java @@ -0,0 +1,17 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.exceptions.ArgumentParseException; + +@FunctionalInterface +public interface ArgumentType { + T parse(String raw) throws ArgumentParseException; + + default boolean isValid(String raw) { + try { + parse(raw); + return true; + } catch (ArgumentParseException ignored) { + return false; + } + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/ArgumentTypeWrapper.java b/src/main/java/fr/zcraft/quartzlib/components/commands/ArgumentTypeWrapper.java new file mode 100644 index 00000000..e253b3de --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/ArgumentTypeWrapper.java @@ -0,0 +1,19 @@ +package fr.zcraft.quartzlib.components.commands; + +class ArgumentTypeWrapper { + private final Class resultType; + private final ArgumentType typeHandler; + + public ArgumentTypeWrapper(Class resultType, ArgumentType typeHandler) { + this.resultType = resultType; + this.typeHandler = typeHandler; + } + + public ArgumentType getTypeHandler() { + return typeHandler; + } + + public Class getResultType() { + return resultType; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/Command.java b/src/main/java/fr/zcraft/quartzlib/components/commands/Command.java deleted file mode 100644 index df37413d..00000000 --- a/src/main/java/fr/zcraft/quartzlib/components/commands/Command.java +++ /dev/null @@ -1,791 +0,0 @@ -/* - * Copyright or © or Copr. QuartzLib contributors (2015 - 2020) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.quartzlib.components.commands; - -import fr.zcraft.quartzlib.components.commands.CommandException.Reason; -import fr.zcraft.quartzlib.components.rawtext.RawText; -import fr.zcraft.quartzlib.core.QuartzLib; -import fr.zcraft.quartzlib.tools.text.RawMessage; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -public abstract class Command { - private static final Pattern FLAG_PATTERN = Pattern.compile("(--?)[a-zA-Z0-9-]+"); - - protected CommandGroup commandGroup; - protected String commandName; - protected String usageParameters; - protected String commandDescription; - protected String[] aliases; - - protected boolean flagsEnabled; - protected Set acceptedFlags; - - protected CommandSender sender; - protected String[] args; - protected Set flags; - - /** - * Parses arguments to extract flags. - * - *

This method is made static and with all data as argument to be able to - * be unit tested.

- * - * @param args The raw arguments. - * @param acceptedFlags A set with lowercase accepted flags. - * @param realArgs An initially empty list filled with the real - * arguments, ordered. - * @param flags An initially empty set filled with flags found in - * the raw arguments. - */ - private static void parseArgs(final String[] args, final Set acceptedFlags, List realArgs, - Set flags) { - for (final String arg : args) { - if (!FLAG_PATTERN.matcher(arg).matches()) { - realArgs.add(arg); - continue; - } - - final Set flagsInArg; - if (arg.startsWith("--")) { - final String flatFlag = arg.replace("--", "").trim().toLowerCase(); - if (isValidFlag(acceptedFlags, flatFlag)) { - flagsInArg = Collections.singleton(flatFlag); - } else { - realArgs.add(arg); - continue; - } - } else { - final String flatFlags = arg.replace("-", "").trim().toLowerCase(); - flagsInArg = new HashSet<>(flatFlags.length()); - - for (char c : flatFlags.toCharArray()) { - final String flag = String.valueOf(c); - if (isValidFlag(acceptedFlags, flag)) { - flagsInArg.add(flag); - } - } - - // If there is no valid flag at all in the argument, we ignore it and - // add it back to args - if (flagsInArg.isEmpty()) { - realArgs.add(arg); - continue; - } - } - - flags.addAll(flagsInArg); - } - } - - /** - * Parses arguments to extract flags (if enabled). - * - * @param args The raw arguments passed to the command. - */ - private void parseArgs(String[] args) { - if (!flagsEnabled) { - this.args = args; - this.flags = null; - return; - } - - final List argsList = new ArrayList<>(args.length); - flags = new HashSet<>(); - - parseArgs(args, acceptedFlags, argsList, flags); - - this.args = argsList.toArray(new String[0]); - } - - /** - * Checks if a flag is accepted. - * - * @param acceptedFlags A list of accepted flags. Can be empty or {@code - * null} accepts all flags while empty accept no one. - * @param flag The flag to test. - * @return {@code true} if this flag is valid. - */ - private static boolean isValidFlag(Set acceptedFlags, String flag) { - return acceptedFlags != null && (acceptedFlags.size() == 0 || acceptedFlags.contains(flag.toLowerCase())); - } - - /** - * Displays a gray informational message. - * - * @param sender The receiver of the message. - * @param message The message to display. - */ - protected static void info(CommandSender sender, String message) { - sender.sendMessage("§7" + message); - } - - /** - * Displays a gray informational message to the sender. - * - * @param message The message to display. - */ - protected void info(String message) { - info(sender, message); - } - - - /** - * Displays a green success message. - * - * @param sender The receiver of the message. - * @param message The message to display. - */ - protected static void success(CommandSender sender, String message) { - sender.sendMessage("§a" + message); - } - - /** - * Displays a green success message to the sender. - * - * @param message The message to display. - */ - protected void success(String message) { - success(sender, message); - } - - /** - * Displays a red warning message. - * - * @param sender The receiver of the message. - * @param message The message to display. - */ - protected static void warning(CommandSender sender, String message) { - sender.sendMessage("§c" + message); - } - - /** - * Displays a red warning message to the sender. - * - * @param message The message to display. - */ - protected void warning(String message) { - warning(sender, message); - } - - private static String invalidParameterString(int index, final String expected) { - return "Argument #" + (index + 1) + " invalid: expected " + expected; - } - - private static String invalidParameterString(int index, final Object[] expected) { - String[] expectedStrings = new String[expected.length]; - - for (int i = expected.length; i-- > 0; ) { - expectedStrings[i] = expected[i].toString().toLowerCase(); - } - - String expectedString = StringUtils.join(expectedStrings, ','); - - return "Argument #" + (index + 1) + " invalid: expected " + expectedString; - } - - /** - * Runs the command. - * - *

Use protected fields to access data (like {@link #args}).

- * - * @throws CommandException If something bad happens. - */ - protected abstract void run() throws CommandException; - - /** - * Initializes the command. Internal use. - * - * @param commandGroup The group this command instance belongs to. - */ - void init(CommandGroup commandGroup) { - this.commandGroup = commandGroup; - - CommandInfo commandInfo = this.getClass().getAnnotation(CommandInfo.class); - - if (commandInfo == null) { - throw new IllegalArgumentException("Command has no CommandInfo annotation"); - } - - commandName = commandInfo.name().toLowerCase(); - usageParameters = commandInfo.usageParameters(); - commandDescription = commandGroup.getDescription(commandName); - aliases = commandInfo.aliases(); - - WithFlags withFlags = this.getClass().getAnnotation(WithFlags.class); - flagsEnabled = withFlags != null; - if (flagsEnabled) { - acceptedFlags = new HashSet<>(); - for (final String flag : withFlags.value()) { - acceptedFlags.add(flag.toLowerCase()); - } - } else { - acceptedFlags = Collections.emptySet(); - } - } - - /** - * Checks if a given sender is allowed to execute this command. - * - * @param sender The sender. - * @return {@code true} if the sender can execute the command. - */ - public boolean canExecute(CommandSender sender) { - String permissionPrefix = QuartzLib.getPlugin().getName().toLowerCase() + "."; - String globalPermission = Commands.getGlobalPermission(); - - if (globalPermission != null) { - if (sender.hasPermission(permissionPrefix + globalPermission)) { - return true; - } - } - - return sender.hasPermission(permissionPrefix + commandGroup.getUsualName()); - } - - /** - * Checks if a given sender is allowed to execute this command. - * - * @param sender The sender. - * @param args The arguments passed to the command. - * @return {@code true} if the sender can execute the command. - */ - public boolean canExecute(CommandSender sender, String[] args) { - return canExecute(sender); - } - - /** - * Tab-completes the command. This command should be overridden. - * - *

Use protected fields to access data (like {@link #args}).

- * - * @return A list with suggestions, or {@code null} without suggestions. - * @throws CommandException If something bad happens. - */ - protected List complete() throws CommandException { - return null; - } - - /** - * Executes this command. - * - * @param sender The sender. - * @param args The raw arguments passed to the command. - */ - public void execute(CommandSender sender, String[] args) { - this.sender = sender; - parseArgs(args); - - try { - if (!canExecute(sender, args)) { - throw new CommandException(this, Reason.SENDER_NOT_AUTHORIZED); - } - run(); - } catch (CommandException ex) { - warning(ex.getReasonString()); - } - - this.sender = null; - this.args = null; - this.flags = null; - } - - /** - * Tab-completes this command. - * - * @param sender The sender. - * @param args The raw arguments passed to the command. - */ - public List tabComplete(CommandSender sender, String[] args) { - List result = null; - - this.sender = sender; - parseArgs(args); - - try { - if (canExecute(sender, args)) { - result = complete(); - } - } catch (CommandException ignored) { } - - this.sender = null; - this.args = null; - this.flags = null; - - if (result == null) { - result = new ArrayList<>(); - } - return result; - } - - /** - * Returns this command's usage parameters. - * @return This command's usage parameters. - */ - public String getUsageParameters() { - return usageParameters; - } - - /** - * Returns this command's usage string. - * @return This command's usage string, formatted like this: {@code - * /{command} {sub-command} {usage parameters}}. - */ - public String getUsageString() { - return "/" + commandGroup.getUsualName() + " " + commandName + " " + usageParameters; - } - - /** - * Returns the name of this command. - * @return The name of this command. - */ - public String getName() { - return commandName; - } - - - ///////////// Common methods for commands ///////////// - - /** - * Get the command group. - * @return The command group this command belongs to. - */ - CommandGroup getCommandGroup() { - return commandGroup; - } - - /** - * Get the aliases. - * @return The aliases of this command. - */ - public String[] getAliases() { - return aliases; - } - - /** - * Checks if the given name matches this command's, or any of its aliases. - * @param name A command name. - * @return {@code true} if this command can be called like that, - * checking (without case) the command name then aliases. - */ - public boolean matches(String name) { - if (commandName.equals(name.toLowerCase())) { - return true; - } - - for (String alias : aliases) { - if (alias.equals(name)) { - return true; - } - } - - return false; - } - - - ///////////// Methods for command execution ///////////// - - /** - * Builds a command usage string. - * @param args Some arguments. - * @return A ready-to-be-executed command string with the passed arguments. - */ - public String build(String... args) { - StringBuilder command = new StringBuilder("/" + commandGroup.getUsualName() + " " + commandName); - - for (String arg : args) { - command.append(" ").append(arg); - } - - return command.toString(); - } - - /** - * Stops the command execution because an argument is invalid, and displays - * an error message. - * - * @param reason The error. - * @throws CommandException the thrown exception. - */ - protected void throwInvalidArgument(String reason) throws CommandException { - throw new CommandException(this, Reason.INVALID_PARAMETERS, reason); - } - - /** - * Stops the command execution because the command usage is disallowed, and - * displays an error message. - * - * @throws CommandException the thrown exception. - */ - protected void throwNotAuthorized() throws CommandException { - throw new CommandException(this, Reason.SENDER_NOT_AUTHORIZED); - } - - /** - * Retrieves the {@link Player} who executed this command. If the command is - * not executed by a player, aborts the execution and displays an error - * message. - * - * @return The player executing this command. - * @throws CommandException If the sender is not a player. - */ - protected Player playerSender() throws CommandException { - if (!(sender instanceof Player)) { - throw new CommandException(this, Reason.COMMANDSENDER_EXPECTED_PLAYER); - } - return (Player) sender; - } - - /** - * Aborts the execution and displays an error message. - * - * @param message The message. - * @throws CommandException the thrown exception. - */ - protected void error(String message) throws CommandException { - throw new CommandException(this, Reason.COMMAND_ERROR, message); - } - - /** - * Aborts the execution and displays a generic error message. - * - * @throws CommandException the thrown exception. - */ - protected void error() throws CommandException { - error(""); - } - - - ///////////// Methods for autocompletion ///////////// - - /** - * Sends a JSON-formatted message to the sender. - * - * @param rawMessage The JSON message. - * @throws CommandException if the command sender is not a player. - */ - protected void tellRaw(String rawMessage) throws CommandException { - RawMessage.send(playerSender(), rawMessage); - } - - /** - * Sends a {@linkplain RawText raw JSON text} to the sender. - * - * @param text The JSON message. - */ - protected void send(RawText text) { - RawMessage.send(sender, text); - } - - /** - * Returns the strings of the list starting with the given prefix. - * - * @param prefix The prefix. - * @param list The strings. - * @return A sub-list containing the strings starting with prefix. - */ - protected List getMatchingSubset(String prefix, String... list) { - return getMatchingSubset(Arrays.asList(list), prefix); - } - - /** - * Returns the strings of the list starting with the given prefix. - * - * @param list The strings. - * @param prefix The prefix. - * @return A sub-list containing the strings starting with prefix. - */ - protected List getMatchingSubset(Iterable list, String prefix) { - List matches = new ArrayList<>(); - - for (String item : list) { - if (item.startsWith(prefix)) { - matches.add(item); - } - } - - return matches; - } - - - ///////////// Methods for parameters ///////////// - - /** - * Returns a list of player names starting by the given prefix, among all - * logged in players. - * - * @param prefix The prefix. - * @return A sub-list containing the players names starting with prefix. - */ - protected List getMatchingPlayerNames(String prefix) { - return getMatchingPlayerNames(Bukkit.getOnlinePlayers(), prefix); - } - - /** - * Returns a list of player names starting by the given prefix, among the - * given players. - * - * @param players A list of players. - * @param prefix The prefix. - * @return A sub-list containing the players names starting with prefix. - */ - protected List getMatchingPlayerNames(Iterable players, String prefix) { - List matches = new ArrayList(); - - for (Player player : players) { - if (player.getName().startsWith(prefix)) { - matches.add(player.getName()); - } - } - - return matches; - } - - /** - * Retrieves an integer at the given index, or aborts the execution if none - * can be found. - * - * @param index The index. - * @return The retrieved integer. - * @throws CommandException If the value is invalid. - */ - protected int getIntegerParameter(int index) throws CommandException { - try { - return Integer.parseInt(args[index]); - } catch (NumberFormatException e) { - throw new CommandException(this, Reason.INVALID_PARAMETERS, invalidParameterString(index, "integer")); - } - } - - /** - * Retrieves a double at the given index, or aborts the execution if none - * can be found. - * - * @param index The index. - * @return The retrieved double. - * @throws CommandException If the value is invalid. - */ - protected double getDoubleParameter(int index) throws CommandException { - try { - return Double.parseDouble(args[index]); - } catch (NumberFormatException e) { - throw new CommandException(this, Reason.INVALID_PARAMETERS, - invalidParameterString(index, "integer or decimal value")); - } - } - - /** - * Retrieves a float at the given index, or aborts the execution if none can - * be found. - * - * @param index The index. - * @return The retrieved float. - * @throws CommandException If the value is invalid. - */ - protected float getFloatParameter(int index) throws CommandException { - try { - return Float.parseFloat(args[index]); - } catch (NumberFormatException e) { - throw new CommandException(this, Reason.INVALID_PARAMETERS, - invalidParameterString(index, "integer or decimal value")); - } - } - - /** - * Retrieves a long at the given index, or aborts the execution if none can - * be found. - * - * @param index The index. - * @return The retrieved long. - * @throws CommandException If the value is invalid. - */ - protected long getLongParameter(int index) throws CommandException { - try { - return Long.parseLong(args[index]); - } catch (NumberFormatException e) { - throw new CommandException(this, Reason.INVALID_PARAMETERS, invalidParameterString(index, "integer")); - } - } - - /** - * Retrieves aa boolean at the given index, or aborts the execution if none - * can be found. - * - *

Accepts yes, y, on, true, 1, no, n, off, false, and 0.

- * - * @param index The index. - * @return The retrieved boolean. - * @throws CommandException If the value is invalid. - */ - protected boolean getBooleanParameter(int index) throws CommandException { - switch (args[index].toLowerCase().trim()) { - case "yes": - case "y": - case "on": - case "true": - case "1": - return true; - - case "no": - case "n": - case "off": - case "false": - case "0": - return false; - - default: - throw new CommandException(this, Reason.INVALID_PARAMETERS, - invalidParameterString(index, "boolean (yes/no)")); - } - } - - /** - * Retrieves an enum value at the given index, or aborts the execution if - * none can be found. - * - *

Checks against the enum values without case, but does not converts - * spaces to underscores or things like that.

- * - * @param index The index. - * @param enumType The enum to search into. - * @return The retrieved enum value. - * @throws CommandException If the value cannot be found in the enum. - */ - protected > T getEnumParameter(int index, Class enumType) throws CommandException { - Enum[] enumValues = enumType.getEnumConstants(); - String parameter = args[index].toLowerCase(); - - for (Enum value : enumValues) { - if (value.toString().toLowerCase().equals(parameter)) { - return (T) value; - } - } - - throw new CommandException(this, Reason.INVALID_PARAMETERS, invalidParameterString(index, enumValues)); - } - - /** - * Retrieves a player from its name at the given index, or aborts the - * execution if none can be found. - * - * @param index The index. - * @return The retrieved player. - * @throws CommandException If the value is invalid. - */ - protected Player getPlayerParameter(int index) throws CommandException { - String parameter = args[index]; - - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.getName().equals(parameter)) { - return player; - } - } - - throw new CommandException(this, Reason.INVALID_PARAMETERS, invalidParameterString(index, "player name")); - } - - /** - * Retrieves a player from its name at the given index, or aborts the - * execution if none can be found. - * - * @param parameter The string containing the name. - * @param callback A consumer that will use the offline player's UUID - */ - public void offlinePlayerParameter(final String parameter, final Consumer callback) { - CommandWorkers cw = new CommandWorkers(); - cw.offlineNameFetch(parameter, callback); - } - - /** - * Retrieves a player from its name at the given index, or aborts the - * execution if none can be found. - * - * @param index The index. - * @param callback A consumer that will use the offline player's UUID - */ - public void offlinePlayerParameter(int index, final Consumer callback) { - final String parameter = args[index]; - offlinePlayerParameter(parameter, callback); - } - - - ///////////// Methods for flags ///////////// - - /** - * Checks if a flag is set. - * - *

To use this functionality, your command class must be annotated by - * {@link WithFlags}.

- * - *

A flag is a value precessed by one or two dashes, and composed of - * alphanumerical characters, and dashes.
Flags are not - * case-sensitive.

- * - *

One-letter flags are passed using the syntax {@code -f} (for the - * {@code f} flag). Multiple one-letter flags can be passed at once, like - * this: {@code -fcrx} (for the {@code f}, {@code c}, {@code r}, and {@code - * x} flags).

- * - *

Multiple-letter flags are passed using the syntax {@code --flag} (for - * the {@code flag} flag). To pass multiple multiple-letter flags, you must - * repeat the {@code --}: {@code --flag --other-flag} (for the flags {@code - * flag} and {@code other-flag}).

- * - *

With the {@link WithFlags} annotation alone, all flags are caught. - * You can constrain the flags retrieved by passing an array of flags to the - * annotation, like this: - * - *

-     *     \@WithFlags({"flag", "f"})
-     * 
- * - *

If a flag-like argument is passed but not in the flags whitelist, it will - * be left in the {@link #args} parameters like any other arguments. Else, - * the retrieved flags are removed from the arguments list.

- * - * @param flag The flag. - * @return {@code true} if the flag was passed by the player. - */ - protected boolean hasFlag(String flag) { - return flags != null && flags.contains(flag.toLowerCase()); - } -} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandEndpoint.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandEndpoint.java new file mode 100644 index 00000000..b400c4f4 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandEndpoint.java @@ -0,0 +1,43 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.exceptions.CommandException; +import java.util.SortedSet; +import java.util.TreeSet; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +class CommandEndpoint extends CommandNode { + private final SortedSet methods = new TreeSet<>(); + + CommandEndpoint(String name) { + super(name, null); + } + + @Override + void run(Object parentInstance, CommandSender sender, String[] args) throws CommandException { + for (CommandMethod method : this.methods) { + if (method.getParameters().length != args.length) { + continue; // TODO + } + + try { + Object[] parsedArgs; + parsedArgs = method.parseArguments(sender, args); + method.run(parentInstance, parsedArgs); + return; + } catch (CommandException ignored) { // TODO + + } + } + + throw new RuntimeException("No matching command found"); // TODO + } + + void addMethod(CommandMethod method) { + methods.add(method); + } + + @NotNull public Iterable getMethods() { + return methods; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandException.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandException.java deleted file mode 100644 index ccb21023..00000000 --- a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandException.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright or © or Copr. QuartzLib contributors (2015 - 2020) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.quartzlib.components.commands; - -import fr.zcraft.quartzlib.components.gui.GuiUtils; -import fr.zcraft.quartzlib.tools.PluginLogger; -import org.bukkit.ChatColor; - - -public class CommandException extends Exception { - private final Reason reason; - private final Command command; - private final String extra; - - /** - * A new CommandException. - * @param command The command that raised the exception. - * @param reason The reason code. - * @param extra Any extra message. - */ - public CommandException(Command command, Reason reason, String extra) { - this.command = command; - this.reason = reason; - this.extra = extra; - } - - public CommandException(Command command, Reason reason) { - this(command, reason, ""); - } - - public Reason getReason() { - return reason; - } - - /** - * Builds the "reason" string for this command exception. - * @return The reason string. - */ - public String getReasonString() { - switch (reason) { - case COMMANDSENDER_EXPECTED_PLAYER: - return "You must be a player to use this command."; - case INVALID_PARAMETERS: - final String prefix = ChatColor.GOLD + Commands.CHAT_PREFIX + " " + ChatColor.RESET; - return "\n" - + ChatColor.RED + Commands.CHAT_PREFIX + ' ' + ChatColor.BOLD + "Invalid argument" + '\n' - + GuiUtils.generatePrefixedFixedLengthString(ChatColor.RED + Commands.CHAT_PREFIX + " ", extra) - + '\n' - + GuiUtils.generatePrefixedFixedLengthString(prefix, "Usage: " + command.getUsageString()) - + '\n' - + GuiUtils.generatePrefixedFixedLengthString(prefix, - "For more information, use /" + command.getCommandGroup().getUsualName() - + " help " + command.getName()); - case COMMAND_ERROR: - return extra.isEmpty() ? "An unknown error suddenly happened." : extra; - case SENDER_NOT_AUTHORIZED: - return "You do not have the permission to use this command."; - default: - PluginLogger.warning("Unknown CommandException caught", this); - return "An unknown error suddenly happened."; - } - } - - public enum Reason { - COMMANDSENDER_EXPECTED_PLAYER, - INVALID_PARAMETERS, - COMMAND_ERROR, - SENDER_NOT_AUTHORIZED - } -} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandGroup.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandGroup.java index f0f984fa..f3ac7a45 100644 --- a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandGroup.java +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandGroup.java @@ -1,346 +1,116 @@ -/* - * Copyright or © or Copr. QuartzLib contributors (2015 - 2020) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - package fr.zcraft.quartzlib.components.commands; -import fr.zcraft.quartzlib.tools.PluginLogger; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.util.ArrayList; +import fr.zcraft.quartzlib.components.commands.exceptions.CommandException; +import fr.zcraft.quartzlib.components.commands.exceptions.MissingSubcommandException; +import fr.zcraft.quartzlib.components.commands.exceptions.UnknownSubcommandException; +import java.lang.reflect.Field; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; -import java.util.List; -import java.util.Scanner; -import org.apache.commons.lang.StringUtils; -import org.bukkit.command.CommandExecutor; +import java.util.Map; +import java.util.function.Supplier; import org.bukkit.command.CommandSender; -import org.bukkit.command.PluginCommand; -import org.bukkit.command.TabCompleter; -import org.bukkit.plugin.java.JavaPlugin; - - -public class CommandGroup implements TabCompleter, CommandExecutor { - private final CommandGroup shortcutCommandGroup; - private final String[] names; - private final Class[] commandsClasses; - private final ArrayList commands = new ArrayList<>(); - private final HashMap commandsDescriptions = new HashMap<>(); - private String description = ""; - - CommandGroup(CommandGroup shortcutCommandGroup, Class commandClass, String... names) { - this.names = names; - this.commandsClasses = new Class[] {commandClass}; - this.shortcutCommandGroup = shortcutCommandGroup; - initCommands(); - } - - CommandGroup(String[] names, Class... commandsClasses) { - this.names = names; - this.commandsClasses = commandsClasses; - this.shortcutCommandGroup = null; - initDescriptions(); - initCommands(); - } - - /** - * Gets the command args from the given group args. - * @param args The group args. - * @return The command args. - */ - public static String[] getCommandArgsFromGroupArgs(String[] args) { - String[] commandArgs = new String[args.length - 1]; - - for (int i = 0; i < commandArgs.length; i++) { - commandArgs[i] = args[i + 1]; - } - - return commandArgs; - } - - private void initDescriptions() { - String fileName = "help/" + getUsualName() + ".txt"; - InputStream stream = getClass().getClassLoader().getResourceAsStream(fileName); - if (stream == null) { - PluginLogger.warning("Could not load description file for the " + getUsualName() + " command"); - return; - } +import org.jetbrains.annotations.Nullable; - Scanner scanner = new Scanner(stream); - StringBuilder builder = new StringBuilder(); +public class CommandGroup extends CommandNode { + private final Class commandGroupClass; - //Getting the group's description - //And then each command's description - int colonIndex; - int firstSpaceIndex; - boolean isGroupDescription = true; - while (scanner.hasNextLine()) { - String line = scanner.nextLine(); - colonIndex = line.indexOf(':'); - if (isGroupDescription) { - firstSpaceIndex = line.indexOf(' '); - if (colonIndex > 0 && firstSpaceIndex > colonIndex) { - isGroupDescription = false; - } - } + @Nullable + private final Supplier classInstanceSupplier; + @Nullable + private final GroupClassInstanceSupplier groupClassInstanceSupplier; - if (isGroupDescription) { - builder.append(line).append('\n'); - } else { - commandsDescriptions.put(line.substring(0, colonIndex).trim(), - line.substring(colonIndex + 1).trim()); - } - } - - scanner.close(); - description = builder.toString().trim(); - - } - - private void initCommands() { - for (Class commandClass : commandsClasses) { - addCommand(commandClass); - } - - if (!isShortcutCommand()) { - addCommand(HelpCommand.class); - } - } + private final Map subCommands = new HashMap<>(); - private void addCommand(Class commandClass) { - Constructor constructor; - Command newCommand; - try { - constructor = commandClass.getConstructor(); - newCommand = constructor.newInstance(); - newCommand.init(isShortcutCommand() ? shortcutCommandGroup : this); - commands.add(newCommand); - } catch (Exception ex) { - PluginLogger.warning("Exception while initializing command", ex); - } + CommandGroup(Class commandGroupClass, Supplier classInstanceSupplier, String name, + TypeCollection typeCollection) { + this(commandGroupClass, classInstanceSupplier, null, name, typeCollection, null); } - /** - * Execute the command matching the args. - * @param sender The command's sender. - * @param args The command args. - * @return true if command ran successfuly. - */ - public boolean executeMatchingCommand(CommandSender sender, String[] args) { - if (isShortcutCommand()) { - commands.get(0).execute(sender, args); - return true; - } - - if (args.length <= 0) { - sender.sendMessage(getUsage()); - return false; - } - - String commandName = args[0]; - String[] commandArgs = getCommandArgsFromGroupArgs(args); - - return executeMatchingCommand(sender, commandName, commandArgs); + CommandGroup(Class commandGroupClass, GroupClassInstanceSupplier classInstanceSupplier, String name, + CommandGroup parent, TypeCollection typeCollection) { + this(commandGroupClass, null, classInstanceSupplier, name, typeCollection, parent); } - private boolean executeMatchingCommand(CommandSender sender, String commandName, String[] args) { - Command command = getMatchingCommand(commandName); - if (command != null) { - command.execute(sender, args); - } else { - sender.sendMessage(getUsage()); - } - return command != null; + CommandGroup(CommandGroup parent, Field backingField, TypeCollection typeCollection) { + this( + backingField.getType(), + GroupClassInstanceSupplier.backingField(backingField), + backingField.getName(), + parent, + typeCollection + ); } - @Override - public List onTabComplete(CommandSender sender, org.bukkit.command.Command cmd, String label, - String[] args) { - return tabComplete(sender, args); + private CommandGroup( + Class commandGroupClass, + @Nullable Supplier classInstanceSupplier, + @Nullable GroupClassInstanceSupplier groupClassInstanceSupplier, String name, + TypeCollection typeCollection, CommandGroup parent) { + super(name, parent); + this.commandGroupClass = commandGroupClass; + this.classInstanceSupplier = classInstanceSupplier; + this.groupClassInstanceSupplier = groupClassInstanceSupplier; + DiscoveryUtils.getCommandMethods(commandGroupClass, typeCollection).forEach(this::addMethod); + DiscoveryUtils.getSubCommands(this, typeCollection).forEach(this::addSubCommand); } - @Override - public boolean onCommand(CommandSender sender, org.bukkit.command.Command cmd, String label, String[] args) { - return executeMatchingCommand(sender, args); + public Collection getSubCommands() { + return this.subCommands.values(); } - /** - * Computes a list of possible autocomplete suggestions for the given partial arguments. - * @param sender The sender of the command. - * @param args The partial arguments. - * @return A list of suggestions. - */ - public List tabComplete(CommandSender sender, String[] args) { - if (isShortcutCommand()) { - return commands.get(0).tabComplete(sender, args); - } - if (args.length <= 1) { - return tabComplete(sender, args.length == 1 ? args[0] : null); - } - String commandName = args[0]; - String[] commandArgs = getCommandArgsFromGroupArgs(args); - return tabCompleteMatching(sender, commandName, commandArgs); + @Nullable public CommandNode getSubCommand(String subCommandName) { + return this.subCommands.get(subCommandName); } - /** - * Computes a list of possible autocomplete suggestions for the given command. - * @param sender The sender of the command. - * @param commandName The name of the command - * @return A list of suggestions. - */ - public List tabComplete(CommandSender sender, String commandName) { - ArrayList matchingCommands = new ArrayList(); - for (Command command : commands) { - if (!command.canExecute(sender)) { - continue; - } - if (commandName == null || command.getName().startsWith(commandName.toLowerCase())) { - matchingCommands.add(command.getName()); - } - } - return matchingCommands; - } - private List tabCompleteMatching(CommandSender sender, String commandName, String[] args) { - Command command = getMatchingCommand(commandName); - if (command != null) { - return command.tabComplete(sender, args); - } else { - return new ArrayList<>(); + private void addMethod(CommandMethod method) { + // TODO: handle adding to non-endpoints + CommandEndpoint endpoint = (CommandEndpoint) subCommands.get(method.getName()); + if (endpoint == null) { + endpoint = new CommandEndpoint(method.getName()); + subCommands.put(endpoint.getName(), endpoint); } + endpoint.addMethod(method); } - /** - * Gets the command matching the given name. - * @param commandName The command name. - * @return The matching command, or null if none were found. - */ - public Command getMatchingCommand(String commandName) { - for (Command command : commands) { - if (command.matches(commandName)) { - return command; - } - } - return null; + private void addSubCommand(CommandGroup commandGroup) { + subCommands.put(commandGroup.getName(), commandGroup); } - /** - * Gets the command matching the given class. - * @param commandClass The command class. - * @return The matching gommand, or null if none were found. - */ - public Command getCommandInfo(Class commandClass) { - for (Command command : commands) { - if (command.getClass().equals(commandClass)) { - return command; - } + void run(CommandSender sender, String... args) throws CommandException { + if (classInstanceSupplier == null) { + throw new IllegalStateException("This command group comes from a parent and cannot instanciate itself."); } - return null; - } - /** - * Return if this command matches the given name. - * @param name The name of the command to test for. - * @return if this command matches the given name. - */ - public boolean matches(String name) { - name = name.toLowerCase(); - for (String commandName : names) { - if (commandName.equals(name)) { - return true; - } - } - return false; + Object commandObject = classInstanceSupplier.get(); + runSelf(commandObject, sender, args); } - /** - * Returns an array of all subcommands. - * @return an array of all subcommands. - */ - public String[] getCommandsNames() { - String[] commandsNames = new String[commands.size()]; - - for (int i = 0; i < commands.size(); i++) { - commandsNames[i] = commands.get(i).getName(); + @Override + void run(Object parentInstance, CommandSender sender, String[] args) throws CommandException { + if (this.groupClassInstanceSupplier == null) { + throw new IllegalStateException("This command group cannot be ran from a parent"); } - return commandsNames; + Object instance = this.groupClassInstanceSupplier.supply(parentInstance); + runSelf(instance, sender, args); } - void register(JavaPlugin plugin) { - PluginCommand bukkitCommand = plugin.getCommand(getUsualName()); - if (bukkitCommand == null) { - throw new IllegalStateException("Command " + getUsualName() + " is not correctly registered in plugin.yml"); + private void runSelf(Object instance, CommandSender sender, String[] args) throws CommandException { + if (args.length == 0) { + throw new MissingSubcommandException(this); } - bukkitCommand.setAliases(getAliases()); - bukkitCommand.setExecutor(this); - bukkitCommand.setTabCompleter(this); - } - protected String getUsage() { - if (isShortcutCommand()) { - return "§cUsage: " + commands.get(0).getUsageString(); + CommandNode subCommand = subCommands.get(args[0]); + if (subCommand == null) { + throw new UnknownSubcommandException(this, args[0]); } - return "§cUsage: /" + getUsualName() - + " <" + StringUtils.join(getCommandsNames(), "|") + ">"; - } - - public String getUsualName() { - return names[0]; - } - - public String[] getNames() { - return names.clone(); - } - - public List getAliases() { - return Arrays.asList(names).subList(1, names.length); - } - public Command[] getCommands() { - return commands.toArray(new Command[commands.size()]); + subCommand.run(instance, sender, Arrays.copyOfRange(args, 1, args.length)); } - public String getDescription() { - return description; + public Class getCommandGroupClass() { + return commandGroupClass; } - - public String getDescription(String commandName) { - return commandsDescriptions.get(commandName); - } - - public boolean isShortcutCommand() { - return shortcutCommandGroup != null; - } - - public CommandGroup getShortcutCommandGroup() { - return shortcutCommandGroup; - } - } diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandInfo.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandInfo.java deleted file mode 100644 index d2060863..00000000 --- a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandInfo.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright or © or Copr. QuartzLib contributors (2015 - 2020) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.quartzlib.components.commands; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) -public @interface CommandInfo { - /** - * The name of the command. - */ - String name(); - - /** - * The "usage" description. - */ - String usageParameters() default ""; - - /** - * Aliases for this command. - */ - String[] aliases() default {}; -} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandManager.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandManager.java new file mode 100644 index 00000000..f48c7a4f --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandManager.java @@ -0,0 +1,41 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.exceptions.CommandException; +import fr.zcraft.quartzlib.core.QuartzLib; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.jetbrains.annotations.Nullable; + +public class CommandManager { + private final Map rootCommands = new HashMap<>(); + private final TypeCollection typeCollection = new TypeCollection(); + + public void addCommand(String name, Class commandType, Supplier commandClassSupplier) { + CommandGroup group = new CommandGroup(commandType, commandClassSupplier, name, typeCollection); + rootCommands.put(name, group); + } + + public void registerCommand(String name, Class commandType, Supplier commandClassSupplier) { + CommandGroup group = new CommandGroup(commandType, commandClassSupplier, name, typeCollection); + rootCommands.put(name, group); + registerCommand(group); + } + + private void registerCommand(CommandGroup group) { + PluginCommand command = QuartzLib.getPlugin().getCommand(group.getName()); + // TODO: handle null here + Objects.requireNonNull(command).setExecutor(new QuartzCommandExecutor(group)); + } + + public void run(CommandSender sender, String commandName, String... args) throws CommandException { + rootCommands.get(commandName).run(sender, args); // TODO + } + + @Nullable public CommandGroup getCommand(String commandName) { + return rootCommands.get(commandName); + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethod.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethod.java new file mode 100644 index 00000000..eeeb0816 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethod.java @@ -0,0 +1,105 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.annotations.Sender; +import fr.zcraft.quartzlib.components.commands.exceptions.ArgumentParseException; +import fr.zcraft.quartzlib.components.commands.exceptions.CommandException; +import fr.zcraft.quartzlib.components.commands.exceptions.InvalidSenderException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.List; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class CommandMethod implements Comparable { + @NotNull private final Method method; + @NotNull private final String name; + @NotNull private final CommandMethodParameter[] parameters; + private final int parameterCount; + @Nullable private CommandMethodSenderArgument senderParameter = null; + + private final int declarationIndex; + private final int priority; + + CommandMethod(Method method, TypeCollection typeCollection, int declarationIndex) { + this.method = method; + this.name = method.getName(); + this.declarationIndex = declarationIndex; + + Parameter[] javaParameters = method.getParameters(); + List parameters = new ArrayList<>(); + for (int i = 0; i < javaParameters.length; i++) { + Parameter parameter = javaParameters[i]; + if (parameter.isAnnotationPresent(Sender.class)) { // TODO: check for multiple sender parameters + senderParameter = new CommandMethodSenderArgument(parameter, i, typeCollection); + } else { + parameters.add(new CommandMethodParameter(this, parameter, i, typeCollection)); + } + } + + this.parameters = parameters.toArray(new CommandMethodParameter[] {}); + this.parameterCount = javaParameters.length; + + fr.zcraft.quartzlib.components.commands.annotations.CommandMethod annotation = + method.getAnnotation(fr.zcraft.quartzlib.components.commands.annotations.CommandMethod.class); + + if (annotation != null) { + priority = annotation.priority(); + } else { + priority = 0; + } + } + + public @NotNull String getName() { + return name; + } + + public void run(Object target, Object[] parsedArgs) throws CommandException { + try { + this.method.invoke(target, parsedArgs); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); // TODO + } + } + + public Object[] parseArguments(CommandSender sender, String[] args) + throws ArgumentParseException, InvalidSenderException { + Object[] parsed = new Object[parameterCount]; + + for (int i = 0; i < parameters.length; i++) { + CommandMethodParameter argument = parameters[i]; + parsed[argument.getPosition()] = argument.parse(args[i]); + } + + if (this.senderParameter != null) { + parsed[this.senderParameter.getPosition()] = this.senderParameter.parse(sender); + } + + return parsed; + } + + public @NotNull CommandMethodParameter[] getParameters() { + return parameters; + } + + public @NotNull Method getMethod() { + return method; + } + + @Override + public int compareTo(@NotNull CommandMethod other) { + if (priority == other.priority) { + if (declarationIndex == other.declarationIndex) { + // This is needed to differentiate between methods with the same declaration index and priority + return method.toString().compareTo(other.method.toString()); + } + + return Integer.compare(declarationIndex, other.declarationIndex); + } + + // Higher priority = first in natural order + return Integer.compare(priority, other.priority) * -1; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethodParameter.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethodParameter.java new file mode 100644 index 00000000..53d2d4f1 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethodParameter.java @@ -0,0 +1,53 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.annotations.Param; +import fr.zcraft.quartzlib.components.commands.exceptions.ArgumentParseException; +import fr.zcraft.quartzlib.components.commands.exceptions.UnknownArgumentTypeException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import org.jetbrains.annotations.NotNull; + +public class CommandMethodParameter { + private final Parameter parameter; + private final int position; + private final ArgumentTypeWrapper typeHandler; + private final String name; + + public CommandMethodParameter( + CommandMethod parent, + Parameter parameter, + int position, + TypeCollection typeCollection + ) { + this.parameter = parameter; + this.position = position; + this.typeHandler = typeCollection.findArgumentType(parameter.getType()) + .orElseThrow(() -> new UnknownArgumentTypeException(parent.getMethod(), parameter.getType())); + this.name = findName(parent.getMethod(), parameter); + } + + public Object parse(String raw) throws ArgumentParseException { + return this.typeHandler.getTypeHandler().parse(raw); + } + + public int getPosition() { + return position; + } + + @NotNull public String getName() { + return name; + } + + private static String findName(Method declaringMethod, Parameter param) { + Param annotation = param.getAnnotation(Param.class); + if (annotation != null) { + return annotation.value(); + } + + if (param.isNamePresent()) { + return param.getName(); + } + + return DiscoveryUtils.generateArgumentName(declaringMethod, param); + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethodSenderArgument.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethodSenderArgument.java new file mode 100644 index 00000000..6c773508 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandMethodSenderArgument.java @@ -0,0 +1,25 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.exceptions.InvalidSenderException; +import java.lang.reflect.Parameter; +import org.bukkit.command.CommandSender; + +public class CommandMethodSenderArgument { + private final Parameter parameter; + private final int position; + private final SenderTypeWrapper typeHandler; + + public CommandMethodSenderArgument(Parameter parameter, int position, TypeCollection typeCollection) { + this.parameter = parameter; + this.position = position; + this.typeHandler = typeCollection.findSenderType(parameter.getType()).get(); // FIXME: handle unknown types + } + + public Object parse(CommandSender raw) throws InvalidSenderException { + return this.typeHandler.getTypeHandler().parse(raw); + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandNode.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandNode.java new file mode 100644 index 00000000..d5371bd0 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandNode.java @@ -0,0 +1,25 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.exceptions.CommandException; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; + +public abstract class CommandNode { + private final String name; + @Nullable private final CommandGroup parent; + + protected CommandNode(String name, @Nullable CommandGroup parent) { + this.name = name; + this.parent = parent; + } + + public String getName() { + return name; + } + + @Nullable public CommandGroup getParent() { + return parent; + } + + abstract void run(Object parentInstance, CommandSender sender, String[] args) throws CommandException; +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandWorkers.java b/src/main/java/fr/zcraft/quartzlib/components/commands/CommandWorkers.java deleted file mode 100644 index b1b596c6..00000000 --- a/src/main/java/fr/zcraft/quartzlib/components/commands/CommandWorkers.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.zcraft.quartzlib.components.commands; - -import fr.zcraft.quartzlib.components.i18n.I; -import fr.zcraft.quartzlib.components.worker.Worker; -import fr.zcraft.quartzlib.components.worker.WorkerAttributes; -import fr.zcraft.quartzlib.components.worker.WorkerCallback; -import fr.zcraft.quartzlib.components.worker.WorkerRunnable; -import fr.zcraft.quartzlib.tools.PluginLogger; -import fr.zcraft.quartzlib.tools.mojang.UUIDFetcher; -import java.util.UUID; -import java.util.function.Consumer; - -@WorkerAttributes(name = "Command's worker", queriesMainThread = true) -public class CommandWorkers extends Worker { - - /** - * Fetches an offline player's UUID by name. - */ - public void offlineNameFetch(final String playerName, final Consumer callback) { - final WorkerCallback wCallback = new WorkerCallback() { - @Override - public void finished(UUID result) { - callback.accept(result); // Si tout va bien on passe l'UUID au callback - } - - @Override - public void errored(Throwable exception) { - PluginLogger.warning(I.t("Error while getting player UUID")); - callback.accept(null); // En cas d'erreur on envoie `null` au callback - } - }; - WorkerRunnable wr = new WorkerRunnable() { - @Override - public UUID run() throws Throwable { - return UUIDFetcher.fetch(playerName); - } - }; - submitQuery(wr, wCallback); - } - -} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/Commands.java b/src/main/java/fr/zcraft/quartzlib/components/commands/Commands.java deleted file mode 100644 index bf7c7ea8..00000000 --- a/src/main/java/fr/zcraft/quartzlib/components/commands/Commands.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright or © or Copr. QuartzLib contributors (2015 - 2020) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.quartzlib.components.commands; - -import fr.zcraft.quartzlib.core.QuartzComponent; -import fr.zcraft.quartzlib.core.QuartzLib; -import java.util.ArrayList; -import java.util.List; -import org.bukkit.command.CommandSender; - -public class Commands extends QuartzComponent { - public static final String CHAT_PREFIX = "┃"; - - private static final List commandGroups = new ArrayList<>(); - private static String globalPermission; - - /** - * Registers a shortcut command. - */ - public static void registerShortcut(String commandGroupName, Class commandClass, - String... shortcutNames) { - CommandGroup group = getMatchingCommandGroup(commandGroupName); - if (group == null) { - throw new IllegalArgumentException("Invalid command group name: " + commandGroupName); - } - CommandGroup newCommandGroup = new CommandGroup(group, commandClass, shortcutNames); - - newCommandGroup.register(QuartzLib.getPlugin()); - commandGroups.add(newCommandGroup); - } - - /** - * Registers many new commands. - * @param names The names of the commands - * @param commandsClasses The matching classes for the commands - */ - public static void register(String[] names, Class... commandsClasses) { - final CommandGroup commandGroup = new CommandGroup(names, commandsClasses); - commandGroup.register(QuartzLib.getPlugin()); - - commandGroups.add(commandGroup); - } - - public static void register(String name, Class... commandsClasses) { - register(new String[] {name}, commandsClasses); - } - - /** - * Executes a registered command. - * @param sender The command sender. - * @param commandName The name of the command. - * @param args The command's arguments. - * @return Whether the command was found. - */ - public static boolean execute(CommandSender sender, String commandName, String[] args) { - CommandGroup commandGroup = getMatchingCommandGroup(commandName); - if (commandGroup == null) { - return false; - } - commandGroup.executeMatchingCommand(sender, args); - return true; - } - - - /** - * Computes a list of possible autocomplete suggestions for the given command. - * @param sender The sender of the command. - * @param commandName The name of the command. - * @param args The partial arguments for the command. - * @return A list of suggestions. - */ - public static List tabComplete(CommandSender sender, String commandName, String[] args) { - CommandGroup commandGroup = getMatchingCommandGroup(commandName); - if (commandGroup == null) { - return new ArrayList<>(); - } - return commandGroup.tabComplete(sender, args); - } - - /** - * Gets the command matching the given class. - * @param commandClass The command class. - * @return The matching gommand, or null if none were found. - */ - public static Command getCommandInfo(Class commandClass) { - Command command = null; - for (CommandGroup commandGroup : commandGroups) { - command = commandGroup.getCommandInfo(commandClass); - if (command != null) { - break; - } - } - return command; - } - - private static CommandGroup getMatchingCommandGroup(String commandName) { - for (CommandGroup commandGroup : commandGroups) { - if (commandGroup.matches(commandName)) { - return commandGroup; - } - } - return null; - } - - - public static String getGlobalPermission() { - return globalPermission; - } - - public static void setGlobalPermission(String permission) { - globalPermission = permission; - } -} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/DiscoveryUtils.java b/src/main/java/fr/zcraft/quartzlib/components/commands/DiscoveryUtils.java new file mode 100644 index 00000000..9cfeeedc --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/DiscoveryUtils.java @@ -0,0 +1,77 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.annotations.SubCommand; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +abstract class DiscoveryUtils { + public static Stream getCommandMethods(Class commandGroupClass, TypeCollection typeCollection) { + // Yay java "lambdas" + AtomicInteger declarationIndex = new AtomicInteger(0); + + return Arrays.stream(commandGroupClass.getDeclaredMethods()) + .filter(m -> hasRunnableModifiers(m.getModifiers())) + .map((Method method) -> new CommandMethod(method, typeCollection, declarationIndex.getAndIncrement())); + } + + public static Stream getSubCommands(CommandGroup commandGroup, TypeCollection typeCollection) { + return Arrays.stream(commandGroup.getCommandGroupClass().getDeclaredFields()) + .filter(m -> hasRunnableModifiers(m.getModifiers())) + .filter(DiscoveryUtils::isSubcommand) + .map(field -> new CommandGroup(commandGroup, field, typeCollection)); + } + + private static boolean isSubcommand(AccessibleObject field) { + return field.isAnnotationPresent(SubCommand.class); + } + + public static Supplier getClassConstructorSupplier(Class commandGroupClass) { + Constructor constructor = commandGroupClass.getDeclaredConstructors()[0]; + return () -> { + try { + return constructor.newInstance(); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + throw new RuntimeException(e); // TODO + } + }; + } + + private static boolean hasRunnableModifiers(int modifiers) { + return Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers); + } + + public static String generateArgumentName(Method declaringMethod, Parameter parameter) { + String parameterTypeName = generateArgumentName(parameter.getType()); + Parameter[] parametersWithSameTypeName = Arrays.stream(declaringMethod.getParameters()) + .filter(p -> parameterTypeName.equals(generateArgumentName(p.getType()))) + .toArray(Parameter[]::new); + + if (parametersWithSameTypeName.length <= 1) { + return parameterTypeName; + } + + int index = IntStream.range(0, parametersWithSameTypeName.length) + .filter(i -> parameter == parametersWithSameTypeName[i]) + .findFirst() // first occurrence + .orElse(-1) + 1; + + return parameterTypeName + index; + } + + private static String generateArgumentName(Class type) { + String parameterTypeName = type.getSimpleName(); + if ("".equals(parameterTypeName)) { + return "arg"; + } + return Character.toLowerCase(parameterTypeName.charAt(0)) + parameterTypeName.substring(1); + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/ExecutionContext.java b/src/main/java/fr/zcraft/quartzlib/components/commands/ExecutionContext.java new file mode 100644 index 00000000..c350bc21 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/ExecutionContext.java @@ -0,0 +1,13 @@ +package fr.zcraft.quartzlib.components.commands; + +import org.bukkit.command.CommandSender; + +public class ExecutionContext { + private final CommandSender sender; + private final String[] fullArgs; + + public ExecutionContext(CommandSender sender, String[] fullArgs) { + this.sender = sender; + this.fullArgs = fullArgs; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/GenericArgumentType.java b/src/main/java/fr/zcraft/quartzlib/components/commands/GenericArgumentType.java new file mode 100644 index 00000000..48dc320f --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/GenericArgumentType.java @@ -0,0 +1,7 @@ +package fr.zcraft.quartzlib.components.commands; + +import java.util.Optional; + +public interface GenericArgumentType { + Optional> getMatchingArgumentType(Class type); +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/GenericSenderType.java b/src/main/java/fr/zcraft/quartzlib/components/commands/GenericSenderType.java new file mode 100644 index 00000000..b633d320 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/GenericSenderType.java @@ -0,0 +1,7 @@ +package fr.zcraft.quartzlib.components.commands; + +import java.util.Optional; + +public interface GenericSenderType { + Optional> getMatchingSenderType(Class type); +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/GroupClassInstanceSupplier.java b/src/main/java/fr/zcraft/quartzlib/components/commands/GroupClassInstanceSupplier.java new file mode 100644 index 00000000..78497219 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/GroupClassInstanceSupplier.java @@ -0,0 +1,17 @@ +package fr.zcraft.quartzlib.components.commands; + +import java.lang.reflect.Field; + +public interface GroupClassInstanceSupplier { + static GroupClassInstanceSupplier backingField(Field field) { + return (instance) -> { + try { + return field.get(instance); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); // TODO + } + }; + } + + Object supply(Object parent); +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/HelpCommand.java b/src/main/java/fr/zcraft/quartzlib/components/commands/HelpCommand.java deleted file mode 100644 index 1ec391b0..00000000 --- a/src/main/java/fr/zcraft/quartzlib/components/commands/HelpCommand.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright or © or Copr. QuartzLib contributors (2015 - 2020) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.quartzlib.components.commands; - -import fr.zcraft.quartzlib.components.gui.GuiUtils; -import fr.zcraft.quartzlib.components.rawtext.RawText; -import fr.zcraft.quartzlib.core.QuartzLib; -import fr.zcraft.quartzlib.tools.PluginLogger; -import fr.zcraft.quartzlib.tools.commands.PaginatedTextView; -import fr.zcraft.quartzlib.tools.text.RawMessage; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Scanner; -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -@CommandInfo(name = "help", usageParameters = "") -public class HelpCommand extends Command { - @Override - protected void run() throws CommandException { - if (args.length < 1) { - groupHelp(1); - } else { - if (args.length == 1 && args[0].startsWith("--page=")) { - try { - groupHelp(Integer.valueOf(args[0].split("=")[1])); - return; - } catch (NumberFormatException ignored) { - } - } - - commandHelp(); - } - } - - private void groupHelp(int page) throws CommandException { - final List displayedCommands = new ArrayList<>(); - - for (Command subCommands : commandGroup.getCommands()) { - if (subCommands.canExecute(sender, args)) { - displayedCommands.add(subCommands); - } - } - - if (sender instanceof Player) { - info(""); - } - - new GroupHelpPagination() - .setData(displayedCommands.toArray(new Command[displayedCommands.size()])) - .setCurrentPage(page) - .display(sender); - } - - private void commandHelp() throws CommandException { - Command command = commandGroup.getMatchingCommand(args[0]); - if (command == null) { - error("The specified command does not exist."); - return; - } - - if (!command.canExecute(sender, args)) { - warning("You do not have the permission to use this command."); - } - - String message = "\n"; - message += GuiUtils.generatePrefixedFixedLengthString("§6" + Commands.CHAT_PREFIX + "§l ", - QuartzLib.getPlugin().getName() + " help for /" + command.getCommandGroup().getUsualName() + " " - + command.getName()) + "\n"; - message += GuiUtils.generatePrefixedFixedLengthString("§6" + Commands.CHAT_PREFIX + " ", - "Usage: §r" + command.getUsageString()) + "\n"; - - try { - String help = getHelpText(command); - if (help.isEmpty()) { - message += "§c" + Commands.CHAT_PREFIX + " There is no help message for this command."; - } else { - message += help; - } - } catch (IOException ex) { - message += "§c" + Commands.CHAT_PREFIX + " Could not read help for this command."; - PluginLogger.warning("Could not read help for the command: " + command.getName(), ex); - } - - sender.sendMessage(message); - } - - private String getHelpText(Command command) throws IOException { - String fileName = "help/" + commandGroup.getUsualName() - + "/" + command.getName() + ".txt"; - - StringBuilder result = new StringBuilder(); - - InputStream stream = getClass().getClassLoader().getResourceAsStream(fileName); - if (stream == null) { - return ""; - } - - Scanner scanner = new Scanner(stream); - - while (scanner.hasNextLine()) { - String line = scanner.nextLine(); - result.append("§l§9" + Commands.CHAT_PREFIX + " §r").append(line).append("\n"); - } - - scanner.close(); - - return result.toString().trim(); - } - - - @Override - protected List complete() throws CommandException { - if (args.length != 1) { - return null; - } - - ArrayList matches = new ArrayList<>(); - - for (Command command : commandGroup.getCommands()) { - if (command.getName().startsWith(args[0])) { - matches.add(command.getName()); - } - } - - return matches; - } - - - private class GroupHelpPagination extends PaginatedTextView { - @Override - protected void displayHeader(CommandSender receiver) { - final String header = ChatColor.BOLD + (commandGroup.getDescription().isEmpty() - ? QuartzLib.getPlugin().getName() + " help for /" + commandGroup.getUsualName() - : commandGroup.getDescription()); - - receiver.sendMessage(receiver instanceof Player - ? GuiUtils.generatePrefixedFixedLengthString( - ChatColor.BLUE + Commands.CHAT_PREFIX + " " + ChatColor.RESET, header) - : header - ); - } - - @Override - protected void displayItem(CommandSender receiver, Command command) { - final String commandName = "/" + commandGroup.getUsualName() + " " + command.getName(); - final String description = commandGroup.getDescription(command.getName()); - - String helpMessage = ChatColor.GOLD + commandName; - if (description != null) { - helpMessage += ChatColor.GOLD + ": " + ChatColor.WHITE + description; - } - - final String formattedHelpMessage = receiver instanceof Player - ? - GuiUtils.generatePrefixedFixedLengthString(ChatColor.GOLD + Commands.CHAT_PREFIX + " ", helpMessage) - : helpMessage; - - RawText helpLine = RawText.fromFormattedString( - formattedHelpMessage, - new RawText().suggest(commandName + " ").hover(new RawText(command.getUsageString())) - ); - - RawMessage.send(receiver, helpLine); - } - - @Override - protected String getCommandToPage(int page) { - return build("--page=" + page); - } - } -} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/QuartzCommandExecutor.java b/src/main/java/fr/zcraft/quartzlib/components/commands/QuartzCommandExecutor.java new file mode 100644 index 00000000..b51c52af --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/QuartzCommandExecutor.java @@ -0,0 +1,27 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.exceptions.CommandException; +import fr.zcraft.quartzlib.tools.text.RawMessage; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class QuartzCommandExecutor implements CommandExecutor { + private final CommandGroup group; + + public QuartzCommandExecutor(CommandGroup group) { + this.group = group; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, + @NotNull String[] args) { + try { + group.run(sender, args); + } catch (CommandException e) { + RawMessage.send(sender, e.display(sender).build()); + } + return true; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/SenderType.java b/src/main/java/fr/zcraft/quartzlib/components/commands/SenderType.java new file mode 100644 index 00000000..dc741693 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/SenderType.java @@ -0,0 +1,9 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.exceptions.InvalidSenderException; +import org.bukkit.command.CommandSender; + +@FunctionalInterface +public interface SenderType { + T parse(CommandSender raw) throws InvalidSenderException; +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/SenderTypeWrapper.java b/src/main/java/fr/zcraft/quartzlib/components/commands/SenderTypeWrapper.java new file mode 100644 index 00000000..ff7ff326 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/SenderTypeWrapper.java @@ -0,0 +1,19 @@ +package fr.zcraft.quartzlib.components.commands; + +class SenderTypeWrapper { + private final Class resultType; + private final SenderType typeHandler; + + public SenderTypeWrapper(Class resultType, SenderType typeHandler) { + this.resultType = resultType; + this.typeHandler = typeHandler; + } + + public SenderType getTypeHandler() { + return typeHandler; + } + + public Class getResultType() { + return resultType; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/TypeCollection.java b/src/main/java/fr/zcraft/quartzlib/components/commands/TypeCollection.java new file mode 100644 index 00000000..c914798b --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/TypeCollection.java @@ -0,0 +1,95 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.components.commands.arguments.generic.EnumArgumentType; +import fr.zcraft.quartzlib.components.commands.arguments.primitive.IntegerArgumentType; +import fr.zcraft.quartzlib.components.commands.senders.GenericCommandSender; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +class TypeCollection { + private final Map, ArgumentTypeWrapper> argumentTypeMap = new HashMap<>(); + private final List> genericArgumentTypes = new ArrayList<>(); + + private final Map, SenderTypeWrapper> senderTypeMap = new HashMap<>(); + private final List> genericSenderTypes = new ArrayList<>(); + + public TypeCollection() { + this.registerNativeTypes(); + } + + public void register(ArgumentTypeWrapper typeHandler) { + argumentTypeMap.put(typeHandler.getResultType(), typeHandler); + } + + public void register(GenericArgumentType genericArgumentType) { + genericArgumentTypes.add(genericArgumentType); + } + + public void register(SenderTypeWrapper typeHandler) { + senderTypeMap.put(typeHandler.getResultType(), typeHandler); + } + + public void register(GenericSenderType genericSenderType) { + genericSenderTypes.add(genericSenderType); + } + + public Optional> findArgumentType(Class resultType) { + ArgumentTypeWrapper typeHandler = argumentTypeMap.get(resultType); + if (typeHandler != null) { + return Optional.of(typeHandler); + } + return this.findGenericArgumentType(resultType); + } + + private Optional> findGenericArgumentType(Class resultType) { + for (GenericArgumentType t : genericArgumentTypes) { + Optional> matchingArgumentType = t.getMatchingArgumentType(resultType); + + if (matchingArgumentType.isPresent()) { + ArgumentTypeWrapper typeHandler = + new ArgumentTypeWrapper<>(resultType, (ArgumentType) matchingArgumentType.get()); + return Optional.of(typeHandler); + } + } + return Optional.empty(); + } + + private void registerNativeTypes() { + // Primitive types + register(new ArgumentTypeWrapper<>(Integer.class, new IntegerArgumentType())); + register(new ArgumentTypeWrapper<>(String.class, s -> s)); + + register(new ArgumentTypeWrapper<>(int.class, new IntegerArgumentType())); + + // Generic types + register(new EnumArgumentType()); + + // Generic sender types + register(new GenericCommandSender()); + } + + + public Optional> findSenderType(Class resultType) { + SenderTypeWrapper typeHandler = senderTypeMap.get(resultType); + if (typeHandler != null) { + return Optional.of(typeHandler); + } + return this.findGenericSenderType(resultType); + } + + private Optional> findGenericSenderType(Class resultType) { + for (GenericSenderType t : genericSenderTypes) { + Optional> matchingSenderType = t.getMatchingSenderType(resultType); + + if (matchingSenderType.isPresent()) { + SenderTypeWrapper typeHandler = + new SenderTypeWrapper<>(resultType, (SenderType) matchingSenderType.get()); + return Optional.of(typeHandler); + } + } + return Optional.empty(); + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/WithFlags.java b/src/main/java/fr/zcraft/quartzlib/components/commands/WithFlags.java deleted file mode 100644 index b8ab047c..00000000 --- a/src/main/java/fr/zcraft/quartzlib/components/commands/WithFlags.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright or © or Copr. AmauryCarrade (2015) - * - * http://amaury.carrade.eu - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.quartzlib.components.commands; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - - -/** - * Adds this annotation to a command class to make it accept flags, - * i.e. parameters prefixed with - or -- extracted from the args - * array and made available through {@link Command#hasFlag(String)}. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) -public @interface WithFlags { - - /** - * The name of the flags. - */ - String[] value() default {}; -} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/CommandMethod.java b/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/CommandMethod.java new file mode 100644 index 00000000..d5dd5930 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/CommandMethod.java @@ -0,0 +1,12 @@ +package fr.zcraft.quartzlib.components.commands.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface CommandMethod { + int priority() default 0; +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/Param.java b/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/Param.java new file mode 100644 index 00000000..ab379ee7 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/Param.java @@ -0,0 +1,12 @@ +package fr.zcraft.quartzlib.components.commands.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface Param { + String value(); +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/Sender.java b/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/Sender.java new file mode 100644 index 00000000..f3b2a95b --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/Sender.java @@ -0,0 +1,11 @@ +package fr.zcraft.quartzlib.components.commands.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface Sender { +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/SubCommand.java b/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/SubCommand.java new file mode 100644 index 00000000..bbdb490f --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/annotations/SubCommand.java @@ -0,0 +1,11 @@ +package fr.zcraft.quartzlib.components.commands.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface SubCommand { +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/arguments/generic/EnumArgumentType.java b/src/main/java/fr/zcraft/quartzlib/components/commands/arguments/generic/EnumArgumentType.java new file mode 100644 index 00000000..77cd208d --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/arguments/generic/EnumArgumentType.java @@ -0,0 +1,60 @@ +package fr.zcraft.quartzlib.components.commands.arguments.generic; + +import fr.zcraft.quartzlib.components.commands.ArgumentType; +import fr.zcraft.quartzlib.components.commands.GenericArgumentType; +import fr.zcraft.quartzlib.components.commands.exceptions.ArgumentParseException; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class EnumArgumentType implements GenericArgumentType> { + private static Map> getEnumValues(Class enumClass) { + Map> enumValues = new HashMap<>(); + + Arrays.stream(enumClass.getDeclaredFields()) + .filter(f -> Modifier.isPublic(f.getModifiers()) + && Modifier.isStatic(f.getModifiers()) + && enumClass.isAssignableFrom(f.getType())) + .forEach(f -> { + try { + f.setAccessible(true); + enumValues.put(f.getName().toLowerCase(), (Enum) f.get(null)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + + return enumValues; + } + + @Override + public Optional>> getMatchingArgumentType(Class type) { + if (type.isEnum()) { + return Optional.of(new DiscreteEnumArgumentType(type)); + } + return Optional.empty(); + } + + private static class DiscreteEnumArgumentType implements ArgumentType> { + private final Map> enumValues; + + public DiscreteEnumArgumentType(Class enumClass) { + enumValues = getEnumValues(enumClass); + } + + @Override + public Enum parse(String raw) throws ArgumentParseException { + Enum value = enumValues.get(raw); + if (value == null) { + throw new EnumParseException(); + } + return value; + } + } + + private static class EnumParseException extends ArgumentParseException { + + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/arguments/primitive/IntegerArgumentType.java b/src/main/java/fr/zcraft/quartzlib/components/commands/arguments/primitive/IntegerArgumentType.java new file mode 100644 index 00000000..a01fa989 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/arguments/primitive/IntegerArgumentType.java @@ -0,0 +1,19 @@ +package fr.zcraft.quartzlib.components.commands.arguments.primitive; + +import fr.zcraft.quartzlib.components.commands.ArgumentType; +import fr.zcraft.quartzlib.components.commands.exceptions.ArgumentParseException; + +public class IntegerArgumentType implements ArgumentType { + @Override + public Integer parse(String raw) throws ArgumentParseException { + try { + return Integer.parseInt(raw, 10); + } catch (NumberFormatException ignored) { + throw new IntegerParseException(); + } + } + + private static class IntegerParseException extends ArgumentParseException { + + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/ArgumentParseException.java b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/ArgumentParseException.java new file mode 100644 index 00000000..c2e20753 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/ArgumentParseException.java @@ -0,0 +1,11 @@ +package fr.zcraft.quartzlib.components.commands.exceptions; + +import fr.zcraft.quartzlib.components.rawtext.RawText; +import org.bukkit.command.CommandSender; + +public class ArgumentParseException extends CommandException { + @Override + public RawText display(CommandSender sender) { + return null; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/CommandException.java b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/CommandException.java new file mode 100644 index 00000000..baa218af --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/CommandException.java @@ -0,0 +1,8 @@ +package fr.zcraft.quartzlib.components.commands.exceptions; + +import fr.zcraft.quartzlib.components.rawtext.RawText; +import org.bukkit.command.CommandSender; + +public abstract class CommandException extends Exception { + public abstract RawText display(CommandSender sender); +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/InvalidSenderException.java b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/InvalidSenderException.java new file mode 100644 index 00000000..ca07d99d --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/InvalidSenderException.java @@ -0,0 +1,11 @@ +package fr.zcraft.quartzlib.components.commands.exceptions; + +import fr.zcraft.quartzlib.components.rawtext.RawText; +import org.bukkit.command.CommandSender; + +public class InvalidSenderException extends CommandException { + @Override + public RawText display(CommandSender sender) { + return null; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/MissingSubcommandException.java b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/MissingSubcommandException.java new file mode 100644 index 00000000..61d32686 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/MissingSubcommandException.java @@ -0,0 +1,63 @@ +package fr.zcraft.quartzlib.components.commands.exceptions; + +import fr.zcraft.quartzlib.components.commands.CommandGroup; +import fr.zcraft.quartzlib.components.commands.CommandNode; +import fr.zcraft.quartzlib.components.i18n.I; +import fr.zcraft.quartzlib.components.rawtext.RawText; +import fr.zcraft.quartzlib.components.rawtext.RawTextPart; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +public class MissingSubcommandException extends CommandException { + private final CommandGroup commandGroup; + + public MissingSubcommandException(CommandGroup commandGroup) { + this.commandGroup = commandGroup; + } + + @Override + public RawText display(CommandSender sender) { + RawTextPart text = new RawText(I.t("Missing subcommand: ")) + .color(ChatColor.RED) + .then("/").color(ChatColor.WHITE) + .then(getParents()).color(ChatColor.AQUA) + .then(" <").style(ChatColor.GRAY) + .then(I.t("sub-command")) + .style(ChatColor.GRAY, ChatColor.UNDERLINE) + .hover(appendSubCommandList(new RawText())) + .then(">").style(ChatColor.GRAY); + + return text.build(); + } + + private String getParents() { + StringBuilder builder = new StringBuilder(); + + CommandGroup group = commandGroup; + + do { + if (builder.length() > 0) { + builder.append(' '); + } + builder.append(group.getName()); + group = group.getParent(); + } while (group != null); + + return builder.toString(); + } + + private RawTextPart appendSubCommandList(RawTextPart text) { + boolean first = true; + text = text.then(I.t("One of the following:\n ")); + for (CommandNode subCommand : commandGroup.getSubCommands()) { + if (!first) { + text = text.then(", ").color(ChatColor.GRAY); + } + first = false; + + text = text.then(subCommand.getName()).color(ChatColor.AQUA); + } + + return text; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/UnknownArgumentTypeException.java b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/UnknownArgumentTypeException.java new file mode 100644 index 00000000..bb0fc275 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/UnknownArgumentTypeException.java @@ -0,0 +1,15 @@ +package fr.zcraft.quartzlib.components.commands.exceptions; + +import java.lang.reflect.Method; + +public class UnknownArgumentTypeException extends RuntimeException { + public UnknownArgumentTypeException(Method method, Class foundType) { + super(getErrorMessage(method, foundType)); + } + + private static String getErrorMessage(Method method, Class foundType) { + return "Found unknown command argument type: '" + foundType + + "' (found in '" + method.toString() + "'). " + + "Did you forget to register it to the CommandManager?"; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/UnknownSubcommandException.java b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/UnknownSubcommandException.java new file mode 100644 index 00000000..f417be01 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/exceptions/UnknownSubcommandException.java @@ -0,0 +1,93 @@ +package fr.zcraft.quartzlib.components.commands.exceptions; + +import fr.zcraft.quartzlib.components.commands.CommandGroup; +import fr.zcraft.quartzlib.components.commands.CommandNode; +import fr.zcraft.quartzlib.components.i18n.I; +import fr.zcraft.quartzlib.components.rawtext.RawText; +import fr.zcraft.quartzlib.components.rawtext.RawTextPart; +import fr.zcraft.quartzlib.tools.text.StringUtils; +import java.util.List; +import java.util.stream.Collectors; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; + +public class UnknownSubcommandException extends CommandException { + public static final String CHAT_PREFIX = "┃ "; + private final CommandGroup commandGroup; + private final String attemptedSubcommand; + + public UnknownSubcommandException(CommandGroup commandGroup, String attemptedSubcommand) { + this.commandGroup = commandGroup; + this.attemptedSubcommand = attemptedSubcommand; + } + + @Override + public RawText display(CommandSender sender) { + RawTextPart text = new RawText(CHAT_PREFIX).color(ChatColor.DARK_RED) + .then(I.t("Unknown subcommand: ")).color(ChatColor.RED) + .then("/").color(ChatColor.WHITE) + .then(getParents() + " ").color(ChatColor.AQUA) + .then(attemptedSubcommand).style(ChatColor.RED, ChatColor.UNDERLINE, ChatColor.BOLD) + .hover(appendSubCommandList(new RawText())); + + String nearest = getNearestCommand(); + + if (nearest != null) { + text = text.then("\n" + CHAT_PREFIX).color(ChatColor.AQUA) + .then(" " + I.t("Did you mean: ")).color(ChatColor.GRAY) + .then("/").color(ChatColor.WHITE) + .then(getParents() + " ").style(ChatColor.AQUA, ChatColor.UNDERLINE) + .hover(I.t("Click to insert this command")) + .suggest("/" + getParents() + " " + nearest) + .then(nearest).style(ChatColor.DARK_AQUA, ChatColor.UNDERLINE) + .hover(I.t("Click to insert this command")) + .suggest("/" + getParents() + " " + nearest); + } + + return text.build(); + } + + private static final int MAX_DISTANCE = 10; + + @Nullable + private String getNearestCommand() { + List names = commandGroup.getSubCommands() + .stream() + .map(CommandNode::getName) + .collect(Collectors.toList()); + + return StringUtils.levenshteinNearest(attemptedSubcommand, names, MAX_DISTANCE); + } + + private String getParents() { + StringBuilder builder = new StringBuilder(); + + CommandGroup group = commandGroup; + + do { + if (builder.length() > 0) { + builder.append(' '); + } + builder.append(group.getName()); + group = group.getParent(); + } while (group != null); + + return builder.toString(); + } + + private RawTextPart appendSubCommandList(RawTextPart text) { + boolean first = true; + text = text.then(I.t("Should be one of the following:\n ")); + for (CommandNode subCommand : commandGroup.getSubCommands()) { + if (!first) { + text = text.then(", ").color(ChatColor.GRAY); + } + first = false; + + text = text.then(subCommand.getName()).color(ChatColor.AQUA); + } + + return text; + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/commands/senders/GenericCommandSender.java b/src/main/java/fr/zcraft/quartzlib/components/commands/senders/GenericCommandSender.java new file mode 100644 index 00000000..ed8d1c68 --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/components/commands/senders/GenericCommandSender.java @@ -0,0 +1,38 @@ +package fr.zcraft.quartzlib.components.commands.senders; + +import fr.zcraft.quartzlib.components.commands.GenericSenderType; +import fr.zcraft.quartzlib.components.commands.SenderType; +import fr.zcraft.quartzlib.components.commands.exceptions.InvalidSenderException; +import java.util.Optional; +import org.bukkit.command.CommandSender; + +public class GenericCommandSender implements GenericSenderType { + @Override + public Optional> getMatchingSenderType(Class type) { + if (CommandSender.class.isAssignableFrom(type)) { + return Optional.of(new CommandSenderSubType(type)); + } + return Optional.empty(); + } + + private static class InvalidSenderTypeException extends InvalidSenderException { + + } + + private static class CommandSenderSubType implements SenderType { + private final Class subtype; + + private CommandSenderSubType(Class subtype) { + this.subtype = subtype; + } + + @Override + public CommandSender parse(CommandSender raw) throws InvalidSenderException { + if (subtype.isAssignableFrom(raw.getClass())) { + return raw; + } + + throw new InvalidSenderTypeException(); + } + } +} diff --git a/src/main/java/fr/zcraft/quartzlib/components/rawtext/RawTextPart.java b/src/main/java/fr/zcraft/quartzlib/components/rawtext/RawTextPart.java index c71faee4..c8d56216 100644 --- a/src/main/java/fr/zcraft/quartzlib/components/rawtext/RawTextPart.java +++ b/src/main/java/fr/zcraft/quartzlib/components/rawtext/RawTextPart.java @@ -32,8 +32,6 @@ package fr.zcraft.quartzlib.components.rawtext; import com.google.common.base.CaseFormat; -import fr.zcraft.quartzlib.components.commands.Command; -import fr.zcraft.quartzlib.components.commands.Commands; import fr.zcraft.quartzlib.tools.PluginLogger; import fr.zcraft.quartzlib.tools.items.ItemUtils; import fr.zcraft.quartzlib.tools.reflection.NMSException; @@ -353,20 +351,21 @@ public T command(String command) { return click(ActionClick.RUN_COMMAND, command); } - /** + /* * Adds a command executed when this text component is clicked. * * @param command The command class to execute on click. * @param args The arguments to pass to the command. * @return The current raw text component, for method chaining. */ - public T command(Class command, String... args) { + // TODO + /*public T command(Class command, String... args) { Command commandInfo = Commands.getCommandInfo(command); if (commandInfo == null) { throw new IllegalArgumentException("Unknown command"); } return command(commandInfo.build(args)); - } + }*/ /** * Adds an URI to be opened when this text component is clicked. @@ -399,7 +398,7 @@ public T suggest(String suggestion) { return click(ActionClick.SUGGEST_COMMAND, suggestion); } - /** + /* * Adds a command to be suggested on click, i.e. the command will be placed * into the player's chat when clicked. * @@ -407,13 +406,14 @@ public T suggest(String suggestion) { * @param args The arguments to pass to the command. * @return The current raw text component, for method chaining. */ - public T suggest(Class command, String... args) { + // TODO + /*public T suggest(Class command, String... args) { Command commandInfo = Commands.getCommandInfo(command); if (commandInfo == null) { throw new IllegalArgumentException("Unknown command"); } return click(ActionClick.SUGGEST_COMMAND, commandInfo.build(args)); - } + }*/ /** * Adds a text to be inserted on shift-click, i.e. the text will be appended @@ -428,7 +428,7 @@ public T insert(String insertion) { return (T) this; } - /** + /* * Adds a command to be inserted on shift-click, i.e. the command will be * appended to the player's chat when shift-clicked. * @@ -436,13 +436,14 @@ public T insert(String insertion) { * @param args The arguments to pass to the command. * @return The current raw text component, for method chaining. */ - public T insert(Class command, String... args) { + // TODO + /*public T insert(Class command, String... args) { Command commandInfo = Commands.getCommandInfo(command); if (commandInfo == null) { throw new IllegalArgumentException("Unknown command"); } return insert(commandInfo.build(args)); - } + }*/ /** * Builds this chain of components into a {@link RawText} ready to be used. diff --git a/src/main/java/fr/zcraft/quartzlib/tools/text/RawMessage.java b/src/main/java/fr/zcraft/quartzlib/tools/text/RawMessage.java index 302338aa..73ba86f8 100644 --- a/src/main/java/fr/zcraft/quartzlib/tools/text/RawMessage.java +++ b/src/main/java/fr/zcraft/quartzlib/tools/text/RawMessage.java @@ -41,7 +41,7 @@ * Utility to send JSON messages. * *

This tool uses the /tellraw command to send the messages. If the JSON is not correctly - * formatted, the message will not be sent and a Runtime exception containing the exception throw by + * formatted, the message will not be sent and a Runtime exception containing the exception thrown by * the vanilla /tellraw command will be thrown.

*/ public final class RawMessage { diff --git a/src/main/java/fr/zcraft/quartzlib/tools/text/StringUtils.java b/src/main/java/fr/zcraft/quartzlib/tools/text/StringUtils.java new file mode 100644 index 00000000..2475b3dc --- /dev/null +++ b/src/main/java/fr/zcraft/quartzlib/tools/text/StringUtils.java @@ -0,0 +1,91 @@ +package fr.zcraft.quartzlib.tools.text; + +import org.jetbrains.annotations.Nullable; + +/** + * Various string-related utilities. + */ +public final class StringUtils { + private StringUtils() { + } + + /** + * Find the nearest from a given string among the given list of candidates, + * computed based on a Levenshtein Distance. + *

The string must be from at most a given maximum distance of all candidates, else null is returned.

+ * @param toTest The string to test. + * @param candidates The list of candidates. + * @param maxDistance The maximum distance. + * @param The type of CharSequence to test (usually String) + * @return The nearest candidate to the string to test, if found within maxDistance. + */ + @Nullable + public static T levenshteinNearest(T toTest, Iterable candidates, int maxDistance) { + T nearest = null; + int nearestDistance = maxDistance; + + for (T subCommand : candidates) { + int distance = StringUtils.levenshteinDistance(toTest, subCommand); + + if (distance < nearestDistance) { + nearest = subCommand; + nearestDistance = distance; + } + } + + return nearest; + } + + /** + * Compute the distance of Levenshtein Distance between two strings. + * + *

Implementation is from: + * https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Java

+ * @param lhs The first string + * @param rhs The second string + * @return The distance between the two strings + */ + public static int levenshteinDistance(CharSequence lhs, CharSequence rhs) { + int len0 = lhs.length() + 1; + int len1 = rhs.length() + 1; + + // the array of distances + int[] cost = new int[len0]; + int[] newcost = new int[len0]; + + // initial cost of skipping prefix in String s0 + for (int i = 0; i < len0; i++) { + cost[i] = i; + } + + // dynamically computing the array of distances + + // transformation cost for each letter in s1 + for (int j = 1; j < len1; j++) { + // initial cost of skipping prefix in String s1 + newcost[0] = j; + + // transformation cost for each letter in s0 + for (int i = 1; i < len0; i++) { + // matching current letters in both strings + int match = (lhs.charAt(i - 1) == rhs.charAt(j - 1)) ? 0 : 1; + + // computing cost for each transformation + int costReplace = cost[i - 1] + match; + int costInsert = cost[i] + 1; + int costDelete = newcost[i - 1] + 1; + + // keep minimum cost + newcost[i] = Math.min(Math.min(costInsert, costDelete), costReplace); + } + + // swap cost/newcost arrays + int[] swap = cost; + cost = newcost; + newcost = swap; + } + + // the distance is the cost for transforming all letters in both strings + return cost[len0 - 1]; + } +} diff --git a/src/test/java/fr/zcraft/quartzlib/components/commands/CommandExecutionTests.java b/src/test/java/fr/zcraft/quartzlib/components/commands/CommandExecutionTests.java new file mode 100644 index 00000000..86cd9c3b --- /dev/null +++ b/src/test/java/fr/zcraft/quartzlib/components/commands/CommandExecutionTests.java @@ -0,0 +1,32 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.MockedToasterTest; +import fr.zcraft.quartzlib.components.commands.exceptions.CommandException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class CommandExecutionTests extends MockedToasterTest { + private CommandManager commands; + + @Before + public void beforeEach() { + commands = new CommandManager(); + } + + @Test + public void canRegisterAndRunCommand() { + final boolean[] ran = {false}; + + class FooCommand { + public void get() { + ran[0] = true; + } + } + + commands.registerCommand("toaster", FooCommand.class, () -> new FooCommand()); + boolean success = server.dispatchCommand(server.addPlayer(), "toaster get"); + Assert.assertTrue(success); + Assert.assertArrayEquals(new boolean[] {true}, ran); + } +} diff --git a/src/test/java/fr/zcraft/quartzlib/components/commands/CommandFlagsTest.java b/src/test/java/fr/zcraft/quartzlib/components/commands/CommandFlagsTest.java deleted file mode 100644 index f94662f8..00000000 --- a/src/test/java/fr/zcraft/quartzlib/components/commands/CommandFlagsTest.java +++ /dev/null @@ -1,473 +0,0 @@ -/* - * Copyright or © or Copr. AmauryCarrade (2015) - * - * http://amaury.carrade.eu - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.quartzlib.components.commands; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; -import junit.framework.Assert; -import org.apache.commons.lang.StringUtils; -import org.junit.jupiter.api.Test; - -public class CommandFlagsTest { - private final Method parseArgsMethod; - - public CommandFlagsTest() throws ReflectiveOperationException { - parseArgsMethod = - Command.class.getDeclaredMethod("parseArgs", String[].class, Set.class, List.class, Set.class); - parseArgsMethod.setAccessible(true); - } - - private void parseArgs(final String[] args, final Set acceptedFlags, List realArgs, - Set flags) { - try { - parseArgsMethod.invoke(null, args, acceptedFlags, realArgs, flags); - } catch (IllegalAccessException | InvocationTargetException e) { - e.printStackTrace(); - Assert.fail("Cannot invoke parseArgs method"); - } - } - - private void assertArgs(final String[] args, final String[] acceptedFlags, final String[] expectedArgs, - final String[] expectedFlags) { - final Set acceptedFlagsSet = acceptedFlags != null ? new HashSet<>(Arrays.asList(acceptedFlags)) : null; - - final List actualArgs = new ArrayList<>(args.length); - final Set actualFlags = new HashSet<>(); - - parseArgs(args, acceptedFlagsSet, actualArgs, actualFlags); - - final TreeSet expectedFlagsSorted = new TreeSet<>(Arrays.asList(expectedFlags)); - final TreeSet actualFlagsSorted = new TreeSet<>(actualFlags); - - Assert.assertEquals("Expected and actual arguments differs", StringUtils.join(expectedArgs, ","), - StringUtils.join(actualArgs, ",")); - Assert.assertEquals("Expected and actual flags differs", StringUtils.join(expectedFlagsSorted, ","), - StringUtils.join(actualFlagsSorted, ",")); - } - - @Test - public void flagsDisabledTest() { - assertArgs( - new String[] {"arg0", "arg1", "arg2"}, - null, - new String[] {"arg0", "arg1", "arg2"}, - new String[] {} - ); - } - - @Test - public void flagsDisabledWithFlagLikeTest() { - assertArgs( - new String[] {"arg0", "arg1", "-flag", "arg2", "--flag2"}, - null, - new String[] {"arg0", "arg1", "-flag", "arg2", "--flag2"}, - new String[] {} - ); - } - - @Test - public void simpleFlagsTest() { - assertArgs( - new String[] {"arg0", "arg1", "-f"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "-l"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"f", "l"} - ); - } - - @Test - public void simpleMultipleFlagsTest() { - assertArgs( - new String[] {"arg0", "arg1", "-fl"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"f", "l"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-flop"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"f", "l", "o", "p"} - ); - } - - @Test - public void longFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--flop"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "--flop", "--pomf"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop", "pomf"} - ); - } - - @Test - public void longFlagWithDashTest() { - assertArgs( - new String[] {"arg0", "arg1", "--flop-pomf"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop-pomf"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "--flop-pomf", "--pomf"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop-pomf", "pomf"} - ); - } - - @Test - public void mixedFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--flop", "-fl", "--pomf"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop", "pomf", "f", "l"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "--flop", "--pomf", "-l"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop", "pomf", "f", "l"} - ); - } - - @Test - public void mixedCaseFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--FLOP", "-fL", "--poMf"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop", "pomf", "f", "l"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-F", "--flop", "--POMf", "-L"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop", "pomf", "f", "l"} - ); - } - - @Test - public void middleFlagTest() { - assertArgs( - new String[] {"arg0", "--flop", "-fl", "arg1", "--pomf"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop", "pomf", "f", "l"} - ); - - assertArgs( - new String[] {"arg0", "-f", "--flop", "arg1", "--pomf", "-l"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"flop", "pomf", "f", "l"} - ); - } - - @Test - public void duplicatedFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--pomf", "--pomf"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"pomf"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "-f"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-ff"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - } - - @Test - public void duplicatedMixedCaseFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--pomf", "--POMF"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"pomf"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "-F"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-fF"}, - new String[] {}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - } - - - // Constrained - - - @Test - public void simpleConstrainedFlagsTest() { - assertArgs( - new String[] {"arg0", "arg1", "-f"}, - new String[] {"f"}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "-l"}, - new String[] {"f"}, - new String[] {"arg0", "arg1", "-l"}, - new String[] {"f"} - ); - } - - @Test - public void simpleMultipleConstrainedFlagsTest() { - assertArgs( - new String[] {"arg0", "arg1", "-fl"}, - new String[] {"f"}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-flop"}, - new String[] {"f", "o"}, - new String[] {"arg0", "arg1"}, - new String[] {"f", "o"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-fl"}, - new String[] {"k"}, - new String[] {"arg0", "arg1", "-fl"}, - new String[] {} - ); - } - - @Test - public void longConstrainedFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--flop"}, - new String[] {"flop"}, - new String[] {"arg0", "arg1"}, - new String[] {"flop"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "--flop", "--pomf"}, - new String[] {"pomf"}, - new String[] {"arg0", "arg1", "--flop"}, - new String[] {"pomf"} - ); - } - - @Test - public void mixedConstrainedFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--flop", "-fl", "--pomf"}, - new String[] {"flop", "f"}, - new String[] {"arg0", "arg1", "--pomf"}, - new String[] {"flop", "f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "--flop", "--pomf", "-l"}, - new String[] {"flop", "f"}, - new String[] {"arg0", "arg1", "--pomf", "-l"}, - new String[] {"flop", "f"} - ); - } - - @Test - public void mixedCaseConstrainedFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--FLOP", "-fL", "--poMf"}, - new String[] {"flop", "l"}, - new String[] {"arg0", "arg1", "--poMf"}, - new String[] {"flop", "l"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-F", "--flop", "--POMf", "-L"}, - new String[] {"flop", "l"}, - new String[] {"arg0", "arg1", "-F", "--POMf"}, - new String[] {"flop", "l"} - ); - } - - @Test - public void middleConstrainedFlagTest() { - assertArgs( - new String[] {"arg0", "--flop", "-fl", "arg1", "--pomf"}, - new String[] {"pomf", "l"}, - new String[] {"arg0", "--flop", "arg1"}, - new String[] {"pomf", "l"} - ); - - assertArgs( - new String[] {"arg0", "-f", "--flop", "arg1", "--pomf", "-l"}, - new String[] {"pomf", "l"}, - new String[] {"arg0", "-f", "--flop", "arg1"}, - new String[] {"pomf", "l"} - ); - } - - @Test - public void duplicatedConstrainedFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--pomf", "--pomf"}, - new String[] {"pomf"}, - new String[] {"arg0", "arg1"}, - new String[] {"pomf"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "-f"}, - new String[] {"f"}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-ff"}, - new String[] {"f"}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "--pomf", "--pomf"}, - new String[] {"flop"}, - new String[] {"arg0", "arg1", "--pomf", "--pomf"}, - new String[] {} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "-f"}, - new String[] {"l"}, - new String[] {"arg0", "arg1", "-f", "-f"}, - new String[] {} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-ff"}, - new String[] {"l"}, - new String[] {"arg0", "arg1", "-ff"}, - new String[] {} - ); - } - - @Test - public void duplicatedMixedCaseConstrainedFlagTest() { - assertArgs( - new String[] {"arg0", "arg1", "--pomf", "--POMF"}, - new String[] {"pomf"}, - new String[] {"arg0", "arg1"}, - new String[] {"pomf"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "-F"}, - new String[] {"f"}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-fF"}, - new String[] {"f"}, - new String[] {"arg0", "arg1"}, - new String[] {"f"} - ); - - assertArgs( - new String[] {"arg0", "arg1", "--pomf", "--POMF"}, - new String[] {"flop"}, - new String[] {"arg0", "arg1", "--pomf", "--POMF"}, - new String[] {} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-f", "-F"}, - new String[] {"l"}, - new String[] {"arg0", "arg1", "-f", "-F"}, - new String[] {} - ); - - assertArgs( - new String[] {"arg0", "arg1", "-fF"}, - new String[] {"l"}, - new String[] {"arg0", "arg1", "-fF"}, - new String[] {} - ); - } -} diff --git a/src/test/java/fr/zcraft/quartzlib/components/commands/CommandGraphTests.java b/src/test/java/fr/zcraft/quartzlib/components/commands/CommandGraphTests.java new file mode 100644 index 00000000..bb9fb28e --- /dev/null +++ b/src/test/java/fr/zcraft/quartzlib/components/commands/CommandGraphTests.java @@ -0,0 +1,277 @@ +package fr.zcraft.quartzlib.components.commands; + +import fr.zcraft.quartzlib.MockedBukkitTest; +import fr.zcraft.quartzlib.components.commands.annotations.CommandMethod; +import fr.zcraft.quartzlib.components.commands.annotations.Param; +import fr.zcraft.quartzlib.components.commands.annotations.Sender; +import fr.zcraft.quartzlib.components.commands.annotations.SubCommand; +import fr.zcraft.quartzlib.components.commands.exceptions.CommandException; +import java.util.Objects; +import java.util.stream.StreamSupport; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class CommandGraphTests extends MockedBukkitTest { + private CommandManager commands; + + @Before + public void beforeEach() { + commands = new CommandManager(); + } + + @Test + public void canDiscoverBasicSubcommands() { + class FooCommand { + public void add() { + } + + public void get() { + } + + public void list() { + } + } + + CommandGroup commandGroup = + new CommandGroup(FooCommand.class, () -> new FooCommand(), "foo", new TypeCollection()); + String[] commandNames = + StreamSupport.stream(commandGroup.getSubCommands().spliterator(), false).map(CommandNode::getName) + .toArray(String[]::new); + Assert.assertArrayEquals(new String[] {"add", "get", "list"}, commandNames); + } + + @Test + public void onlyDiscoversPublicMethods() { + CommandGroup commandGroup = + new CommandGroup(CommandWithStatics.class, () -> new CommandWithStatics(), "foo", new TypeCollection()); + String[] commandNames = + StreamSupport.stream(commandGroup.getSubCommands().spliterator(), false).map(CommandNode::getName) + .toArray(String[]::new); + Assert.assertArrayEquals(new String[] {"add", "delete"}, commandNames); + } + + @Test + public void canRunBasicSubcommands() throws CommandException { + final boolean[] ran = {false, false, false}; + + class FooCommand { + public void add() { + ran[0] = true; + } + + public void get() { + ran[1] = true; + } + + public void list() { + ran[2] = true; + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + commands.run(server.addPlayer(), "foo", "get"); + Assert.assertArrayEquals(new boolean[] {false, true, false}, ran); + } + + @Test + public void canReceiveStringArguments() throws CommandException { + final String[] argValue = {""}; + + class FooCommand { + public void add(String arg) { + argValue[0] = arg; + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + commands.run(server.addPlayer(), "foo", "add", "pomf"); + Assert.assertArrayEquals(new String[] {"pomf"}, argValue); + } + + @Test + public void canReceiveParsedArguments() throws CommandException { + final int[] argValue = {0}; + + class FooCommand { + public void add(Integer arg) { + argValue[0] = arg; + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + commands.run(server.addPlayer(), "foo", "add", "42"); + Assert.assertArrayEquals(new int[] {42}, argValue); + } + + @Test + public void canReceiveEnumArguments() throws CommandException { + final FooEnum[] argValue = {null}; + + class FooCommand { + public void add(FooEnum arg) { + argValue[0] = arg; + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + commands.run(server.addPlayer(), "foo", "add", "foo"); + Assert.assertArrayEquals(new FooEnum[] {FooEnum.FOO}, argValue); + commands.run(server.addPlayer(), "foo", "add", "bar"); + Assert.assertArrayEquals(new FooEnum[] {FooEnum.BAR}, argValue); + } + + @Test + public void canReceiveCommandSender() throws CommandException { + final CommandSender[] senders = {null}; + Player player = server.addPlayer(); + + class FooCommand { + public void add(@Sender CommandSender sender) { + senders[0] = sender; + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + commands.run(player, "foo", "add"); + Assert.assertArrayEquals(new CommandSender[] {player}, senders); + } + + @Test + public void canCallSubcommand() throws CommandException { + final boolean[] ran = {false}; + + class SubFooCommand { + public void add() { + ran[0] = true; + } + } + + class FooCommand { + @SubCommand + public final SubFooCommand sub = new SubFooCommand(); + + public void add() { + throw new RuntimeException("This shouldn't run!"); + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + commands.run(server.addPlayer(), "foo", "sub", "add"); + Assert.assertArrayEquals(new boolean[] {true}, ran); + } + + @Test + public void canHandleOverrides() throws CommandException { + final String[] argValue = {""}; + + class FooCommand { + public void add(String arg) { + argValue[0] = arg; + } + + public void add() { + argValue[0] = "bar"; + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + + commands.run(server.addPlayer(), "foo", "add", "pomf"); + Assert.assertArrayEquals(new String[] {"pomf"}, argValue); + + commands.run(server.addPlayer(), "foo", "add"); + Assert.assertArrayEquals(new String[] {"bar"}, argValue); + } + + @Test + public void canHandleOverridesWithSameArgumentCount() throws CommandException { + final Object[] argValue = {null}; + + class FooCommand { + public void add(Integer arg) { + argValue[0] = arg; + } + + public void add(String arg) { + argValue[0] = arg; + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + + commands.run(server.addPlayer(), "foo", "add", "pomf"); + Assert.assertArrayEquals(new Object[] {"pomf"}, argValue); + + commands.run(server.addPlayer(), "foo", "add", "42"); + Assert.assertArrayEquals(new Object[] {42}, argValue); + } + + @Test + public void canHandleOverridesWithPriorities() throws CommandException { + final Object[] argValue = {null}; + + class FooCommand { + public void add(String arg) { + argValue[0] = arg; + } + + public void add(Integer arg) { + argValue[0] = arg; + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + + commands.run(server.addPlayer(), "foo", "add", "pomf"); + Assert.assertArrayEquals(new Object[] {"pomf"}, argValue); + + commands.run(server.addPlayer(), "foo", "add", "42"); + Assert.assertArrayEquals(new Object[] {"42"}, argValue); + + class FooCommand2 { + public void add(String arg) { + argValue[0] = arg; + } + + @CommandMethod(priority = 2) + public void add(Integer arg) { + argValue[0] = arg; + } + } + + commands.addCommand("foo", FooCommand2.class, () -> new FooCommand2()); + + commands.run(server.addPlayer(), "foo", "add", "pomf"); + Assert.assertArrayEquals(new Object[] {"pomf"}, argValue); + + commands.run(server.addPlayer(), "foo", "add", "42"); + Assert.assertArrayEquals(new Object[] {42}, argValue); + } + + enum FooEnum { + FOO, BAR + } + + @Test + public void canSpecifyParam() throws CommandException { + class FooCommand { + public void add(String arg) { + } + + public void list(@Param("myInt") Integer arg) { + } + } + + commands.addCommand("foo", FooCommand.class, () -> new FooCommand()); + CommandGroup fooCommand = Objects.requireNonNull(commands.getCommand("foo")); + + CommandEndpoint addCommand = (CommandEndpoint) Objects.requireNonNull(fooCommand.getSubCommand("add")); + Assert.assertEquals("string", addCommand.getMethods().iterator().next().getParameters()[0].getName()); + + CommandEndpoint listCommand = (CommandEndpoint) Objects.requireNonNull(fooCommand.getSubCommand("list")); + Assert.assertEquals("myInt", listCommand.getMethods().iterator().next().getParameters()[0].getName()); + } +} diff --git a/src/test/java/fr/zcraft/quartzlib/components/commands/CommandWithStatics.java b/src/test/java/fr/zcraft/quartzlib/components/commands/CommandWithStatics.java new file mode 100644 index 00000000..1fda8472 --- /dev/null +++ b/src/test/java/fr/zcraft/quartzlib/components/commands/CommandWithStatics.java @@ -0,0 +1,22 @@ +package fr.zcraft.quartzlib.components.commands; + +// This is outside because inner classes cannot have statics +class CommandWithStatics { + public static void staticMethod() { + } + + public void add() { + } + + private void get() { + } + + protected void list() { + } + + public void delete() { + } + + void update() { + } +} diff --git a/src/test/java/fr/zcraft/quartzlib/components/commands/DiscoveryUtilsTests.java b/src/test/java/fr/zcraft/quartzlib/components/commands/DiscoveryUtilsTests.java new file mode 100644 index 00000000..7f21e3ef --- /dev/null +++ b/src/test/java/fr/zcraft/quartzlib/components/commands/DiscoveryUtilsTests.java @@ -0,0 +1,32 @@ +package fr.zcraft.quartzlib.components.commands; + +import java.lang.reflect.Method; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class DiscoveryUtilsTests { + @Test + public void canGenerateArgumentNames() throws NoSuchMethodException { + class Foo { + public void add(int foo1, String foo2) { + + } + + public void add(String foo1, String foo2) { + + } + } + + Method addMethod = Foo.class.getMethod("add", int.class, String.class); + Assertions.assertEquals("int", + DiscoveryUtils.generateArgumentName(addMethod, addMethod.getParameters()[0])); + Assertions.assertEquals("string", + DiscoveryUtils.generateArgumentName(addMethod, addMethod.getParameters()[1])); + + Method add2Method = Foo.class.getMethod("add", String.class, String.class); + Assertions.assertEquals("string1", + DiscoveryUtils.generateArgumentName(add2Method, add2Method.getParameters()[0])); + Assertions.assertEquals("string2", + DiscoveryUtils.generateArgumentName(add2Method, add2Method.getParameters()[1])); + } +} diff --git a/src/test/java/fr/zcraft/quartzlib/components/commands/arguments/generic/EnumArgumentTypeTests.java b/src/test/java/fr/zcraft/quartzlib/components/commands/arguments/generic/EnumArgumentTypeTests.java new file mode 100644 index 00000000..699cfa5c --- /dev/null +++ b/src/test/java/fr/zcraft/quartzlib/components/commands/arguments/generic/EnumArgumentTypeTests.java @@ -0,0 +1,24 @@ +package fr.zcraft.quartzlib.components.commands.arguments.generic; + +import fr.zcraft.quartzlib.components.commands.ArgumentType; +import fr.zcraft.quartzlib.components.commands.exceptions.ArgumentParseException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class EnumArgumentTypeTests { + private final EnumArgumentType enumArgumentType = new EnumArgumentType(); + + private enum SimpleEnum { + FOO, BAR + } + + @Test + public void worksOnSimpleEnum() throws ArgumentParseException { + ArgumentType argumentType = enumArgumentType.getMatchingArgumentType(SimpleEnum.class).get(); + + Assertions.assertEquals(SimpleEnum.FOO, argumentType.parse("foo")); + Assertions.assertEquals(SimpleEnum.BAR, argumentType.parse("bar")); + + Assertions.assertThrows(ArgumentParseException.class, () -> argumentType.parse("blah")); + } +} diff --git a/src/test/java/fr/zcraft/quartzlib/tools/text/StringUtilsTests.java b/src/test/java/fr/zcraft/quartzlib/tools/text/StringUtilsTests.java new file mode 100644 index 00000000..0456389b --- /dev/null +++ b/src/test/java/fr/zcraft/quartzlib/tools/text/StringUtilsTests.java @@ -0,0 +1,29 @@ +package fr.zcraft.quartzlib.tools.text; + +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; + +public class StringUtilsTests { + @Test + public void canComputeLevenshteinDistance() { + Assertions.assertEquals(0, StringUtils.levenshteinDistance("foo", "foo")); + Assertions.assertEquals(1, StringUtils.levenshteinDistance("fooa", "foo")); + Assertions.assertEquals(1, StringUtils.levenshteinDistance("foo", "fooa")); + Assertions.assertEquals(1, StringUtils.levenshteinDistance("fao", "foo")); + Assertions.assertEquals(6, StringUtils.levenshteinDistance("fooaaaaaa", "foo")); + Assertions.assertEquals(2, StringUtils.levenshteinDistance("f", "foo")); + Assertions.assertEquals(3, StringUtils.levenshteinDistance("a", "foo")); + } + + @Test + public void canFindLevenshteinNearest() { + List candidates = Arrays.asList("add", "list", "open"); + + Assertions.assertEquals("add", StringUtils.levenshteinNearest("foo", candidates, 10)); + Assertions.assertEquals("add", StringUtils.levenshteinNearest("adf", candidates, 10)); + Assertions.assertEquals("open", StringUtils.levenshteinNearest("openn", candidates, 10)); + Assertions.assertNull(StringUtils.levenshteinNearest("kkkkkkkkkkkkkkkk", candidates, 10)); + } +} diff --git a/ztoaster/src/main/java/fr/zcraft/ztoaster/ToastCommands.java b/ztoaster/src/main/java/fr/zcraft/ztoaster/ToastCommands.java new file mode 100644 index 00000000..d38baea1 --- /dev/null +++ b/ztoaster/src/main/java/fr/zcraft/ztoaster/ToastCommands.java @@ -0,0 +1,59 @@ +package fr.zcraft.ztoaster; + +import fr.zcraft.quartzlib.components.commands.annotations.Sender; +import fr.zcraft.quartzlib.components.gui.Gui; +import fr.zcraft.quartzlib.components.i18n.I; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class ToastCommands { + + // public ToastSubCommand subCommand = new ToastSubCommand(); // ... + + public void add(@Sender Player cook) { + Toast toast = ToasterWorker.addToast(cook); + cook.sendMessage(I.t("Toast {0} added.", toast.getToastId())); + } + + public void add(@Sender Player cook, int toastCount) { + for (int i = toastCount; i-- > 0; ) { + ToasterWorker.addToast(cook); + } + + cook.sendMessage(I.tn("One toast added.", "{0} toasts added.", toastCount, toastCount)); + } + + public void list(@Sender CommandSender sender) { + showToasts(sender, Arrays.asList(Toaster.getToasts())); + } + + public void list(@Sender CommandSender sender, Toast.CookingStatus cookingStatus) { + ArrayList toasts = new ArrayList(); + + for (Toast toast : Toaster.getToasts()) { + if (toast.getStatus().equals(cookingStatus)) { + toasts.add(toast); + } + } + + showToasts(sender, toasts); + } + + private void showToasts(CommandSender sender, Collection toasts) { + if (toasts.isEmpty()) { + // Output of the command /toaster list, without toasts. + sender.sendMessage("§7" + I.t("There are no toasts here ...")); + } + + for (Toast toast : toasts) { + sender.sendMessage(I.t(" Toast #{0}", toast.getToastId())); + } + } + + public void open(@Sender Player player) { + Gui.open(player, new ToastExplorer()); + } +} diff --git a/ztoaster/src/main/java/fr/zcraft/ztoaster/Toaster.java b/ztoaster/src/main/java/fr/zcraft/ztoaster/Toaster.java index 199b3a20..7b221da3 100644 --- a/ztoaster/src/main/java/fr/zcraft/ztoaster/Toaster.java +++ b/ztoaster/src/main/java/fr/zcraft/ztoaster/Toaster.java @@ -30,16 +30,13 @@ package fr.zcraft.ztoaster; -import fr.zcraft.quartzlib.components.commands.Commands; +import fr.zcraft.quartzlib.components.commands.CommandManager; import fr.zcraft.quartzlib.components.gui.Gui; import fr.zcraft.quartzlib.components.i18n.I18n; import fr.zcraft.quartzlib.components.scoreboard.Sidebar; import fr.zcraft.quartzlib.components.scoreboard.SidebarScoreboard; import fr.zcraft.quartzlib.core.QuartzPlugin; import fr.zcraft.quartzlib.tools.PluginLogger; -import fr.zcraft.ztoaster.commands.AddCommand; -import fr.zcraft.ztoaster.commands.ListCommand; -import fr.zcraft.ztoaster.commands.OpenCommand; import java.io.File; import java.util.ArrayList; import java.util.Locale; @@ -68,7 +65,8 @@ public class Toaster extends QuartzPlugin implements Listener { */ private Sidebar toasterSidebar; - public Toaster () {} + public Toaster() { + } protected Toaster(JavaPluginLoader loader, PluginDescriptionFile description, File dataFolder, File file) { super(loader, description, dataFolder, file); @@ -76,6 +74,7 @@ protected Toaster(JavaPluginLoader loader, PluginDescriptionFile description, Fi /** * . + * * @return The id for a new toast. */ public static int newToastId() { @@ -84,6 +83,7 @@ public static int newToastId() { /** * . + * * @return an array of all the toasts ever created (until toaster restart). */ public static Toast[] getToasts() { @@ -108,9 +108,10 @@ public void onEnable() { toasts = new ArrayList<>(); toastCounter = 0; - loadComponents(Gui.class, Commands.class, ToasterWorker.class, SidebarScoreboard.class, I18n.class); + loadComponents(Gui.class, ToasterWorker.class, SidebarScoreboard.class, I18n.class); - Commands.register("toaster", AddCommand.class, OpenCommand.class, ListCommand.class); + new CommandManager() + .registerCommand("toaster", ToastCommands.class, ToastCommands::new); I18n.useDefaultPrimaryLocale(); I18n.setFallbackLocale(Locale.US); diff --git a/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/AddCommand.java b/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/AddCommand.java deleted file mode 100644 index 639c6c1a..00000000 --- a/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/AddCommand.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright or © or Copr. ZLib contributors (2015) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.ztoaster.commands; - -import fr.zcraft.quartzlib.components.commands.Command; -import fr.zcraft.quartzlib.components.commands.CommandException; -import fr.zcraft.quartzlib.components.commands.CommandInfo; -import fr.zcraft.quartzlib.components.i18n.I; -import fr.zcraft.ztoaster.Toast; -import fr.zcraft.ztoaster.ToasterWorker; -import org.bukkit.entity.Player; - - -@CommandInfo(name = "add", usageParameters = "[toast count]") -public class AddCommand extends Command { - @Override - protected void run() throws CommandException { - Player cook = playerSender(); - - if (args.length == 0) { - Toast toast = ToasterWorker.addToast(cook); - cook.sendMessage(I.t("Toast {0} added.", toast.getToastId())); - } else { - int toastCount = getIntegerParameter(0); - for (int i = toastCount; i-- > 0; ) { - ToasterWorker.addToast(cook); - } - - cook.sendMessage(I.tn("One toast added.", "{0} toasts added.", toastCount, toastCount)); - } - } -} diff --git a/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/ListCommand.java b/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/ListCommand.java deleted file mode 100644 index 9e73fcd4..00000000 --- a/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/ListCommand.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright or © or Copr. ZLib contributors (2015) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - -package fr.zcraft.ztoaster.commands; - -import fr.zcraft.quartzlib.components.commands.Command; -import fr.zcraft.quartzlib.components.commands.CommandException; -import fr.zcraft.quartzlib.components.commands.CommandInfo; -import fr.zcraft.quartzlib.components.i18n.I; -import fr.zcraft.ztoaster.Toast; -import fr.zcraft.ztoaster.Toaster; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; - -@CommandInfo(name = "list", usageParameters = "[cooked|not_cooked]") -public class ListCommand extends Command { - @Override - protected void run() throws CommandException { - if (args.length == 0) { - showToasts(Arrays.asList(Toaster.getToasts())); - } else { - ArrayList toasts = new ArrayList(); - Toast.CookingStatus status = getEnumParameter(0, Toast.CookingStatus.class); - - for (Toast toast : Toaster.getToasts()) { - if (toast.getStatus().equals(status)) { - toasts.add(toast); - } - } - - showToasts(toasts); - } - } - - private void showToasts(Collection toasts) { - if (toasts.isEmpty()) { - // Output of the command /toaster list, without toasts. - info(I.t("There are no toasts here ...")); - } - - for (Toast toast : toasts) { - sender.sendMessage(I.t(" Toast #{0}", toast.getToastId())); - } - } -} diff --git a/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/OpenCommand.java b/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/OpenCommand.java deleted file mode 100644 index ee088321..00000000 --- a/ztoaster/src/main/java/fr/zcraft/ztoaster/commands/OpenCommand.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright or © or Copr. ZLib contributors (2015) - * - * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-B - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-B license and that you accept its terms. - */ - - -package fr.zcraft.ztoaster.commands; - -import fr.zcraft.quartzlib.components.commands.Command; -import fr.zcraft.quartzlib.components.commands.CommandException; -import fr.zcraft.quartzlib.components.commands.CommandInfo; -import fr.zcraft.quartzlib.components.gui.Gui; -import fr.zcraft.ztoaster.ToastExplorer; - -@CommandInfo(name = "open") -public class OpenCommand extends Command { - @Override - protected void run() throws CommandException { - Gui.open(playerSender(), new ToastExplorer()); - } -}