diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java index 8a28d241..8b73bdb3 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java @@ -39,6 +39,7 @@ import com.intellectualsites.commands.meta.CommandMeta; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -101,7 +102,10 @@ public final class CommandTree { public Optional> parse(@Nonnull final CommandContext commandContext, @Nonnull final Queue args) throws NoSuchCommandException, NoPermissionException, InvalidSyntaxException { - final Optional> commandOptional = parseCommand(commandContext, args, this.internalTree); + final Optional> commandOptional = parseCommand(new ArrayList<>(), + commandContext, + args, + this.internalTree); commandOptional.flatMap(Command::getSenderType).ifPresent(requiredType -> { if (!requiredType.isAssignableFrom(commandContext.getSender().getClass())) { throw new InvalidCommandSenderException(commandContext.getSender(), requiredType, Collections.emptyList()); @@ -110,7 +114,8 @@ public final class CommandTree { return commandOptional; } - private Optional> parseCommand(@Nonnull final CommandContext commandContext, + private Optional> parseCommand(@Nonnull final List> parsedArguments, + @Nonnull final CommandContext commandContext, @Nonnull final Queue commandQueue, @Nonnull final Node> root) { String permission = this.isPermitted(commandContext.getSender(), root); @@ -121,7 +126,10 @@ public final class CommandTree { .collect(Collectors.toList())); } - final Optional> parsedChild = this.attemptParseUnambiguousChild(commandContext, root, commandQueue); + final Optional> parsedChild = this.attemptParseUnambiguousChild(parsedArguments, + commandContext, + root, + commandQueue); // noinspection all if (parsedChild != null) { return parsedChild; @@ -136,9 +144,7 @@ public final class CommandTree { } else { /* Too many arguments. We have a unique path, so we can send the entire context */ throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() - .apply(root.getValue() - .getOwningCommand() - .getArguments()), + .apply(parsedArguments, root), commandContext.getSender(), this.getChain(root) .stream() .map(Node::getValue) @@ -147,10 +153,7 @@ public final class CommandTree { } else { /* Too many arguments. We have a unique path, so we can send the entire context */ throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() - .apply(Objects.requireNonNull( - Objects.requireNonNull(root.getValue()) - .getOwningCommand()) - .getArguments()), + .apply(parsedArguments, root), commandContext.getSender(), this.getChain(root) .stream() .map(Node::getValue) @@ -164,21 +167,32 @@ public final class CommandTree { if (child.getValue() != null) { final ArgumentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); if (result.getParsedValue().isPresent()) { - return this.parseCommand(commandContext, commandQueue, child); + parsedArguments.add(child.getValue()); + return this.parseCommand(parsedArguments, commandContext, commandQueue, child); } /*else if (result.getFailure().isPresent() && root.children.size() == 1) { }*/ } } } /* We could not find a match */ - throw new NoSuchCommandException(commandContext.getSender(), - getChain(root).stream().map(Node::getValue).collect(Collectors.toList()), - stringOrEmpty(commandQueue.peek())); + if (root.equals(this.internalTree)) { + throw new NoSuchCommandException(commandContext.getSender(), + getChain(root).stream().map(Node::getValue).collect(Collectors.toList()), + stringOrEmpty(commandQueue.peek())); + } + /* We have already traversed the tree */ + throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() + .apply(parsedArguments, root), + commandContext.getSender(), this.getChain(root) + .stream() + .map(Node::getValue) + .collect(Collectors.toList())); } } @Nullable - private Optional> attemptParseUnambiguousChild(@Nonnull final CommandContext commandContext, + private Optional> attemptParseUnambiguousChild(@Nonnull final List> parsedArguments, + @Nonnull final CommandContext commandContext, @Nonnull final Node> root, @Nonnull final Queue commandQueue) { String permission; @@ -204,18 +218,25 @@ public final class CommandTree { throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() .apply(Objects.requireNonNull( child.getValue().getOwningCommand()) - .getArguments()), + .getArguments(), child), commandContext.getSender(), this.getChain(root) .stream() .map(Node::getValue) .collect(Collectors.toList())); } else { + /* throw new NoSuchCommandException(commandContext.getSender(), this.getChain(root) .stream() .map(Node::getValue) .collect(Collectors.toList()), - ""); + "");*/ + throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() + .apply(parsedArguments, root), + commandContext.getSender(), this.getChain(root) + .stream() + .map(Node::getValue) + .collect(Collectors.toList())); } } final ArgumentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); @@ -227,9 +248,7 @@ public final class CommandTree { } else { /* Too many arguments. We have a unique path, so we can send the entire context */ throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() - .apply(Objects.requireNonNull(child.getValue() - .getOwningCommand()) - .getArguments()), + .apply(parsedArguments, child), commandContext.getSender(), this.getChain(root) .stream() .map(Node::getValue) @@ -237,7 +256,8 @@ public final class CommandTree { Collectors.toList())); } } else { - return this.parseCommand(commandContext, commandQueue, child); + parsedArguments.add(child.getValue()); + return this.parseCommand(parsedArguments, commandContext, commandQueue, child); } } else if (result.getFailure().isPresent()) { throw new ArgumentParseException(result.getFailure().get(), commandContext.getSender(), diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandSyntaxFormatter.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandSyntaxFormatter.java index 2a150be0..f4c9b437 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandSyntaxFormatter.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandSyntaxFormatter.java @@ -23,9 +23,10 @@ // package com.intellectualsites.commands.arguments; +import com.intellectualsites.commands.CommandTree; + import javax.annotation.Nonnull; import java.util.List; -import java.util.function.Function; /** * Utility that formats chains of {@link CommandArgument command arguments} into syntax strings @@ -33,10 +34,17 @@ import java.util.function.Function; * @param Command sender type */ @FunctionalInterface -public interface CommandSyntaxFormatter extends Function>, String> { +public interface CommandSyntaxFormatter { - @Override + /** + * Format the command arguments into a syntax string + * + * @param commandArguments Command arguments + * @param node Trailing node + * @return Syntax string + */ @Nonnull - String apply(@Nonnull List> commandArguments); + String apply(@Nonnull List> commandArguments, + @Nonnull CommandTree.Node> node); } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StandardCommandSyntaxFormatter.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StandardCommandSyntaxFormatter.java index b8cc903b..3faf3d49 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StandardCommandSyntaxFormatter.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StandardCommandSyntaxFormatter.java @@ -23,6 +23,8 @@ // package com.intellectualsites.commands.arguments; +import com.intellectualsites.commands.CommandTree; + import javax.annotation.Nonnull; import java.util.Iterator; import java.util.List; @@ -41,7 +43,8 @@ public class StandardCommandSyntaxFormatter implements CommandSyntaxFormatter @Nonnull @Override - public final String apply(@Nonnull final List> commandArguments) { + public final String apply(@Nonnull final List> commandArguments, + @Nonnull final CommandTree.Node> node) { final StringBuilder stringBuilder = new StringBuilder(); final Iterator> iterator = commandArguments.iterator(); while (iterator.hasNext()) { @@ -59,6 +62,27 @@ public class StandardCommandSyntaxFormatter implements CommandSyntaxFormatter stringBuilder.append(" "); } } + CommandTree.Node> tail = node; + while (tail != null && !tail.isLeaf()) { + if (tail.getChildren().size() > 1) { + stringBuilder.append(" "); + final Iterator>> childIterator = tail.getChildren().iterator(); + while (childIterator.hasNext()) { + final CommandTree.Node> child = childIterator.next(); + stringBuilder.append(child.getValue().getName()); + if (childIterator.hasNext()) { + stringBuilder.append("|"); + } + } + break; + } + final CommandArgument argument = tail.getChildren().get(0).getValue(); + stringBuilder.append(" ") + .append(argument.isRequired() ? '<' : '[') + .append(argument.getName()) + .append(argument.isRequired() ? '>' : ']'); + tail = tail.getChildren().get(0); + } return stringBuilder.toString(); } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/InvalidSyntaxException.java b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/InvalidSyntaxException.java index 1831362f..987b4170 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/InvalidSyntaxException.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/InvalidSyntaxException.java @@ -66,14 +66,4 @@ public class InvalidSyntaxException extends CommandParseException { return String.format("Invalid command syntax. Correct syntax is: %s", this.correctSyntax); } - @Override - public final synchronized Throwable fillInStackTrace() { - return this; - } - - @Override - public final synchronized Throwable initCause(final Throwable cause) { - return this; - } - } diff --git a/cloud-core/src/test/java/com/intellectualsites/commands/CommandTreeTest.java b/cloud-core/src/test/java/com/intellectualsites/commands/CommandTreeTest.java index 470f355f..254f09b2 100644 --- a/cloud-core/src/test/java/com/intellectualsites/commands/CommandTreeTest.java +++ b/cloud-core/src/test/java/com/intellectualsites/commands/CommandTreeTest.java @@ -114,6 +114,12 @@ class CommandTreeTest { manager.executeCommand(new TestCommandSender(), "default 5").join(); } + @Test + void invalidCommand() { + Assertions.assertThrows(CompletionException.class, () -> manager + .executeCommand(new TestCommandSender(), "invalid test").join()); + } + public static final class SpecificCommandSender extends TestCommandSender { } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/BukkitCommand.java b/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/BukkitCommand.java index 0afbf2cc..f400a50e 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/BukkitCommand.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/BukkitCommand.java @@ -25,16 +25,28 @@ package com.intellectualsites.commands; import com.intellectualsites.commands.arguments.CommandArgument; import com.intellectualsites.commands.arguments.StaticArgument; +import com.intellectualsites.commands.exceptions.ArgumentParseException; +import com.intellectualsites.commands.exceptions.InvalidCommandSenderException; +import com.intellectualsites.commands.exceptions.InvalidSyntaxException; +import com.intellectualsites.commands.exceptions.NoPermissionException; +import com.intellectualsites.commands.exceptions.NoSuchCommandException; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.command.PluginIdentifiableCommand; import org.bukkit.plugin.Plugin; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.List; +import java.util.function.Consumer; final class BukkitCommand extends org.bukkit.command.Command implements PluginIdentifiableCommand { + private static final String MESSAGE_NO_PERMS = ChatColor.RED + + "I'm sorry, but you do not have permission to perform this command. " + + "Please contact the server administrators if you believe that this is in error."; + private static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command. Type \"/help\" for help."; + private final CommandArgument command; private final BukkitCommandManager bukkitCommandManager; private final com.intellectualsites.commands.Command cloudCommand; @@ -63,13 +75,24 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi builder.toString()) .whenComplete(((commandResult, throwable) -> { if (throwable != null) { - commandSender.sendMessage(ChatColor.RED + throwable.getMessage()); - commandSender.sendMessage(ChatColor.RED + throwable.getCause().getMessage()); - throwable.printStackTrace(); - throwable.getCause().printStackTrace(); - } else { - // Do something... - commandSender.sendMessage("All good!"); + if (throwable instanceof InvalidSyntaxException) { + commandSender.sendMessage(ChatColor.RED + "Invalid Command Syntax. " + + "Correct command syntax is: " + + ChatColor.GRAY + "/" + + ((InvalidSyntaxException) throwable).getCorrectSyntax()); + } else if (throwable instanceof InvalidCommandSenderException) { + commandSender.sendMessage(ChatColor.RED + throwable.getMessage()); + } else if (throwable instanceof NoPermissionException) { + commandSender.sendMessage(MESSAGE_NO_PERMS); + } else if (throwable instanceof NoSuchCommandException) { + commandSender.sendMessage(MESSAGE_UNKNOWN_COMMAND); + } else if (throwable instanceof ArgumentParseException) { + commandSender.sendMessage(ChatColor.RED + "Invalid Command Argument: " + + ChatColor.GRAY + throwable.getCause().getMessage()); + } else { + commandSender.sendMessage(throwable.getMessage()); + throwable.printStackTrace(); + } } })); return true; @@ -101,4 +124,25 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi return this.cloudCommand.getCommandPermission(); } + @Nullable + private E captureException(@Nonnull final Class clazz, @Nullable final Throwable throwable) { + if (throwable == null) { + return null; + } + if (clazz.equals(throwable.getClass())) { + //noinspection unchecked + return (E) throwable; + } + return captureException(clazz, throwable.getCause()); + } + + private boolean handleException(@Nullable final E throwable, + @Nonnull final Consumer consumer) { + if (throwable == null) { + return false; + } + consumer.accept(throwable); + return true; + } + }