From f2aeae334c35f682301d27832c4d680deebd97bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Fri, 11 Sep 2020 09:54:32 +0200 Subject: [PATCH] Some minor code cleanup --- .../commands/CommandManager.java | 3 +- .../commands/CommandTree.java | 163 +++++++++++------- .../exceptions/AmbiguousNodeException.java | 108 ++++++++++++ .../exceptions/CommandParseException.java | 1 + .../exceptions/InvalidSyntaxException.java | 1 + .../exceptions/NoCommandInLeafException.java | 54 ++++++ .../exceptions/NoPermissionException.java | 1 + .../exceptions/NoSuchCommandException.java | 1 + .../commands/meta/SimpleCommandMeta.java | 1 + 9 files changed, 267 insertions(+), 66 deletions(-) create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/exceptions/AmbiguousNodeException.java create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoCommandInLeafException.java diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java b/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java index 61669157..d18b8698 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java @@ -45,8 +45,9 @@ import java.util.function.Function; * The manager is responsible for command registration, parsing delegation, etc. * * @param Command sender type - * @param Commamd meta type + * @param Command meta type */ +@SuppressWarnings("unused") public abstract class CommandManager { private final CommandContextFactory commandContextFactory = new StandardCommandContextFactory<>(); 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 5fed4b07..8936f0bc 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java @@ -27,8 +27,10 @@ import com.intellectualsites.commands.components.CommandComponent; import com.intellectualsites.commands.components.StaticComponent; import com.intellectualsites.commands.components.parser.ComponentParseResult; import com.intellectualsites.commands.context.CommandContext; +import com.intellectualsites.commands.exceptions.AmbiguousNodeException; import com.intellectualsites.commands.exceptions.ComponentParseException; import com.intellectualsites.commands.exceptions.InvalidSyntaxException; +import com.intellectualsites.commands.exceptions.NoCommandInLeafException; import com.intellectualsites.commands.exceptions.NoPermissionException; import com.intellectualsites.commands.exceptions.NoSuchCommandException; import com.intellectualsites.commands.internal.CommandRegistrationHandler; @@ -61,10 +63,10 @@ public class CommandTree { private final Node> internalTree = new Node<>(null); private final CommandManager commandManager; - private final CommandRegistrationHandler commandRegistrationHandler; + private final CommandRegistrationHandler commandRegistrationHandler; private CommandTree(@Nonnull final CommandManager commandManager, - @Nonnull final CommandRegistrationHandler commandRegistrationHandler) { + @Nonnull final CommandRegistrationHandler commandRegistrationHandler) { this.commandManager = commandManager; this.commandRegistrationHandler = commandRegistrationHandler; } @@ -81,13 +83,23 @@ public class CommandTree { @Nonnull public static CommandTree newTree( @Nonnull final CommandManager commandManager, - @Nonnull final CommandRegistrationHandler commandRegistrationHandler) { + @Nonnull final CommandRegistrationHandler commandRegistrationHandler) { return new CommandTree<>(commandManager, commandRegistrationHandler); } + /** + * Attempt to parse string input into a command + * + * @param commandContext Command context instance + * @param args Input + * @return Parsed command, if one could be found + * @throws NoSuchCommandException If there is no command matching the input + * @throws NoPermissionException If the sender lacks permission to execute the command + * @throws InvalidSyntaxException If the command syntax is invalid + */ public Optional> parse(@Nonnull final CommandContext commandContext, @Nonnull final Queue args) throws - NoSuchCommandException { + NoSuchCommandException, NoPermissionException, InvalidSyntaxException { return parseCommand(commandContext, args, this.internalTree); } @@ -101,6 +113,68 @@ public class CommandTree { .map(Node::getValue) .collect(Collectors.toList())); } + + final Optional> parsedChild = this.attemptParseUnambiguousChild(commandContext, root, commandQueue); + // noinspection all + if (parsedChild != null) { + return parsedChild; + } + + /* There are 0 or more static components as children. No variable child components are present */ + if (root.children.isEmpty()) { + /* We are at the bottom. Check if there's a command attached, in which case we're done */ + if (root.getValue() != null && root.getValue().getOwningCommand() != null) { + if (commandQueue.isEmpty()) { + return Optional.ofNullable(this.cast(root.getValue().getOwningCommand())); + } 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() + .getComponents()), + commandContext.getCommandSender(), this.getChain(root) + .stream() + .map(Node::getValue) + .collect(Collectors.toList())); + } + } 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()) + .getComponents()), + commandContext.getCommandSender(), this.getChain(root) + .stream() + .map(Node::getValue) + .collect(Collectors.toList())); + } + } else { + final Iterator>> childIterator = root.getChildren().iterator(); + if (childIterator.hasNext()) { + while (childIterator.hasNext()) { + final Node> child = childIterator.next(); + if (child.getValue() != null) { + final ComponentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); + if (result.getParsedValue().isPresent()) { + return this.parseCommand(commandContext, commandQueue, child); + } /*else if (result.getFailure().isPresent() && root.children.size() == 1) { + }*/ + } + } + } + /* We could not find a match */ + throw new NoSuchCommandException(commandContext.getCommandSender(), + getChain(root).stream().map(Node::getValue).collect(Collectors.toList()), + stringOrEmpty(commandQueue.peek())); + } + } + + @Nullable + private Optional> attemptParseUnambiguousChild(@Nonnull final CommandContext commandContext, + @Nonnull final Node> root, + @Nonnull final Queue commandQueue) { + String permission; final List>> children = root.getChildren(); if (children.size() == 1 && !(children.get(0).getValue() instanceof StaticComponent)) { // The value has to be a variable @@ -166,60 +240,16 @@ public class CommandTree { } } } - /* There are 0 or more static components as children. No variable child components are present */ - if (children.isEmpty()) { - /* We are at the bottom. Check if there's a command attached, in which case we're done */ - if (root.getValue() != null && root.getValue().getOwningCommand() != null) { - if (commandQueue.isEmpty()) { - return Optional.ofNullable(this.cast(root.getValue().getOwningCommand())); - } 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() - .getComponents()), - commandContext.getCommandSender(), this.getChain(root) - .stream() - .map(Node::getValue) - .collect(Collectors.toList())); - } - } 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()) - .getComponents()), - commandContext.getCommandSender(), this.getChain(root) - .stream() - .map(Node::getValue) - .collect(Collectors.toList())); - } - } else { - final Iterator>> childIterator = root.getChildren().iterator(); - if (childIterator.hasNext()) { - while (childIterator.hasNext()) { - final Node> child = childIterator.next(); - if (child.getValue() != null) { - final ComponentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); - if (result.getParsedValue().isPresent()) { - return this.parseCommand(commandContext, commandQueue, child); - } else if (result.getFailure().isPresent() && root.children.size() == 1) { - } - } - } - } - /* We could not find a match */ - throw new NoSuchCommandException(commandContext.getCommandSender(), - getChain(root).stream().map(Node::getValue).collect(Collectors.toList()), - stringOrEmpty(commandQueue.peek())); - } + // noinspection all + return null; } + @Nonnull public List getSuggestions(@Nonnull final CommandContext context, @Nonnull final Queue commandQueue) { return getSuggestions(context, commandQueue, this.internalTree); } + @Nonnull public List getSuggestions(@Nonnull final CommandContext commandContext, @Nonnull final Queue commandQueue, @Nonnull final Node> root) { @@ -268,8 +298,8 @@ public class CommandTree { final ComponentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); if (result.getParsedValue().isPresent()) { return this.getSuggestions(commandContext, commandQueue, child); - } else if (result.getFailure().isPresent() && root.children.size() == 1) { - } + } /* else if (result.getFailure().isPresent() && root.children.size() == 1) { + }*/ } } } @@ -330,7 +360,7 @@ public class CommandTree { return sender.hasPermission( Objects.requireNonNull(Objects.requireNonNull(node.value, "node.value").getOwningCommand(), "owning command").getCommandPermission()) - ? null : node.value.getOwningCommand().getCommandPermission(); + ? null : Objects.requireNonNull(node.value.getOwningCommand(), "owning command").getCommandPermission(); } /* if any of the children would permit the execution, then the sender has a valid @@ -359,14 +389,17 @@ public class CommandTree { throw new IllegalStateException("Top level command component cannot be a variable"); } }); + this.checkAmbiguity(this.internalTree); + // Verify that all leaf nodes have command registered this.getLeaves(this.internalTree).forEach(leaf -> { if (leaf.getOwningCommand() == null) { - // TODO: Custom exception type - throw new IllegalStateException("Leaf node does not have associated owning command"); + throw new NoCommandInLeafException(leaf); } else { - this.commandRegistrationHandler.registerCommand(leaf.getOwningCommand()); + // noinspection unchecked + final Command owningCommand = (Command) leaf.getOwningCommand(); + this.commandRegistrationHandler.registerCommand(owningCommand); } }); @@ -391,18 +424,18 @@ public class CommandTree { } } }); - /* TODO: Figure out a way to register all combinations along a command component path */ } - private void checkAmbiguity(@Nonnull final Node> node) { + private void checkAmbiguity(@Nonnull final Node> node) throws AmbiguousNodeException { if (node.isLeaf()) { return; } final int size = node.children.size(); for (final Node> child : node.children) { if (child.getValue() != null && !child.getValue().isRequired() && size > 1) { - // TODO: Use a custom exception type here - throw new IllegalStateException("Ambiguous command node found: " + node.getValue()); + throw new AmbiguousNodeException(node.getValue(), + child.getValue(), + node.getChildren().stream().map(Node::getValue).collect(Collectors.toList())); } } node.children.forEach(this::checkAmbiguity); @@ -443,12 +476,12 @@ public class CommandTree { return chain; } - @Nullable private Command cast(@Nullable final Command command) { + @Nullable + private Command cast(@Nullable final Command command) { if (command == null) { return null; } - @SuppressWarnings("unchecked") - final Command casted = (Command) command; + @SuppressWarnings("unchecked") final Command casted = (Command) command; return casted; } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/AmbiguousNodeException.java b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/AmbiguousNodeException.java new file mode 100644 index 00000000..30a1f2b5 --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/AmbiguousNodeException.java @@ -0,0 +1,108 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands.exceptions; + +import com.intellectualsites.commands.components.CommandComponent; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Iterator; +import java.util.List; + +/** + * Exception thrown when a {@link com.intellectualsites.commands.context.CommandContext} + * is being inserted into a {@link com.intellectualsites.commands.CommandTree} and an ambiguity + * is detected. + */ +@SuppressWarnings("unused") +public final class AmbiguousNodeException extends IllegalStateException { + + private final CommandComponent parentNode; + private final CommandComponent ambiguousNode; + private final List> children; + + /** + * Construct a new ambiguous node exception + * + * @param parentNode Parent node + * @param ambiguousNode Node that caused exception + * @param children All children of the parent + */ + public AmbiguousNodeException(@Nullable final CommandComponent parentNode, + @Nonnull final CommandComponent ambiguousNode, + @Nonnull final List> children) { + this.parentNode = parentNode; + this.ambiguousNode = ambiguousNode; + this.children = children; + } + + /** + * Get the parent node + * + * @return Parent node + */ + @Nullable + public CommandComponent getParentNode() { + return this.parentNode; + } + + /** + * Get the ambiguous node + * + * @return Ambiguous node + */ + @Nonnull + public CommandComponent getAmbiguousNode() { + return this.ambiguousNode; + } + + /** + * Get all children of the parent + * + * @return Child nodes + */ + @Nonnull + public List> getChildren() { + return this.children; + } + + @Override + public String getMessage() { + final StringBuilder stringBuilder = new StringBuilder("Ambiguous Node: ") + .append(ambiguousNode.getName()) + .append(" cannot be added as a child to ") + .append(parentNode == null ? "" : parentNode.getName()) + .append(" (All children: "); + final Iterator> childIterator = this.children.iterator(); + while (childIterator.hasNext()) { + stringBuilder.append(childIterator.next().getName()); + if (childIterator.hasNext()) { + stringBuilder.append(", "); + } + } + return stringBuilder.append(")") + .toString(); + } + +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/CommandParseException.java b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/CommandParseException.java index 64cf293d..f48341b8 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/CommandParseException.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/CommandParseException.java @@ -33,6 +33,7 @@ import java.util.List; /** * Exception thrown when parsing user input into a command */ +@SuppressWarnings("unused") public class CommandParseException extends IllegalArgumentException { private final CommandSender commandSender; 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 c7905a7d..6ec5cc86 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 @@ -32,6 +32,7 @@ import java.util.List; /** * Exception sent when a {@link CommandSender} inputs invalid command syntax */ +@SuppressWarnings("unused") public class InvalidSyntaxException extends CommandParseException { private final String correctSyntax; diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoCommandInLeafException.java b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoCommandInLeafException.java new file mode 100644 index 00000000..d253ba9e --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoCommandInLeafException.java @@ -0,0 +1,54 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package com.intellectualsites.commands.exceptions; + +import com.intellectualsites.commands.components.CommandComponent; + +import javax.annotation.Nonnull; + +/** + * Thrown when a {@link com.intellectualsites.commands.components.CommandComponent} + * that is registered as a leaf node, does not contain an owning {@link com.intellectualsites.commands.Command} + */ +@SuppressWarnings("unused") +public final class NoCommandInLeafException extends IllegalStateException { + + private final CommandComponent commandComponent; + + public NoCommandInLeafException(@Nonnull final CommandComponent commandComponent) { + super(String.format("Leaf node '%s' does not have associated owning command", commandComponent.getName())); + this.commandComponent = commandComponent; + } + + /** + * Get the command component + * + * @return Command component + */ + @Nonnull + public CommandComponent getCommandComponent() { + return this.commandComponent; + } + +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoPermissionException.java b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoPermissionException.java index b5e3548d..5b258a3a 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoPermissionException.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoPermissionException.java @@ -33,6 +33,7 @@ import java.util.List; * Exception thrown when a {@link CommandSender} misses a permission required * to execute a {@link com.intellectualsites.commands.Command} */ +@SuppressWarnings("unused") public class NoPermissionException extends CommandParseException { private final String missingPermission; diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoSuchCommandException.java b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoSuchCommandException.java index 2d5d9d50..ed58b244 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoSuchCommandException.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/NoSuchCommandException.java @@ -33,6 +33,7 @@ import java.util.List; * Exception thrown when a command sender tries to execute * a command that doesn't exist */ +@SuppressWarnings("unused") public class NoSuchCommandException extends CommandParseException { private final String suppliedCommand; diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/meta/SimpleCommandMeta.java b/cloud-core/src/main/java/com/intellectualsites/commands/meta/SimpleCommandMeta.java index a31f2f26..888fbad0 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/meta/SimpleCommandMeta.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/meta/SimpleCommandMeta.java @@ -33,6 +33,7 @@ import java.util.Optional; /** * A simple immutable string-string map containing command meta */ +@SuppressWarnings("unused") public class SimpleCommandMeta extends CommandMeta { private final Map metaMap;