diff --git a/build.sh b/build.sh index 71bcb8a3..06eefcc5 100755 --- a/build.sh +++ b/build.sh @@ -3,4 +3,6 @@ # Make sure all submodules are initialized git submodule update --remote # Package all jars -./mvnw clean package + +export MAVEN_OPTS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1" +./mvnw -T1C clean package -DskipTests=true diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/Command.java b/cloud-core/src/main/java/com/intellectualsites/commands/Command.java index e9ddd9b9..c0c082ae 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/Command.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/Command.java @@ -226,6 +226,16 @@ public class Command { return this.arguments.get(argument).getDescription(); } + @Override + public final String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + for (final CommandArgument argument : this.getArguments()) { + stringBuilder.append(argument.getName()).append(' '); + } + final String build = stringBuilder.toString(); + return build.substring(0, build.length() - 1); + } + /** * Check whether or not the command is hidden * @@ -274,7 +284,7 @@ public class Command { */ @Nonnull public Builder meta(@Nonnull final String key, @Nonnull final String value) { - final CommandMeta commandMeta = SimpleCommandMeta.builder().with(this.commandMeta).build(); + final CommandMeta commandMeta = SimpleCommandMeta.builder().with(this.commandMeta).with(key, value).build(); return new Builder<>(this.commandManager, commandMeta, this.senderType, this.commandArguments, this.commandExecutionHandler, this.commandPermission); } 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 0ea87411..04ce633b 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java @@ -56,6 +56,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -73,12 +74,13 @@ import java.util.function.Function; @SuppressWarnings("unused") public abstract class CommandManager { + private final Map, BiConsumer> exceptionHandlers = Maps.newHashMap(); + private final EnumSet managerSettings = EnumSet.of(ManagerSettings.ENFORCE_INTERMEDIARY_PERMISSIONS); + private final CommandContextFactory commandContextFactory = new StandardCommandContextFactory<>(); private final ServicePipeline servicePipeline = ServicePipeline.builder().build(); private final ParserRegistry parserRegistry = new StandardParserRegistry<>(); - private final Map, BiConsumer> exceptionHandlers = Maps.newHashMap(); private final Collection> commands = Lists.newLinkedList(); - private final CommandExecutionCoordinator commandExecutionCoordinator; private final CommandTree commandTree; @@ -552,4 +554,42 @@ public abstract class CommandManager { return new CommandHelpHandler<>(this); } + /** + * Get a command manager setting + * + * @param setting Setting + * @return {@code true} if the setting is activated or {@code false} if it's not + */ + public boolean getSetting(@Nonnull final ManagerSettings setting) { + return this.managerSettings.contains(setting); + } + + /** + * Set the setting + * + * @param setting Setting to set + * @param value Value + */ + public void setSetting(@Nonnull final ManagerSettings setting, + final boolean value) { + if (value) { + this.managerSettings.add(setting); + } else { + this.managerSettings.remove(setting); + } + } + + + /** + * Configurable command related settings + */ + public enum ManagerSettings { + /** + * Do not create a compound permission and do not look greedily + * for child permission values, if a preceding command in the tree path + * has a command handler attached + */ + ENFORCE_INTERMEDIARY_PERMISSIONS + } + } 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 5816c020..265284b1 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java @@ -190,8 +190,7 @@ public final class CommandTree { if (result.getParsedValue().isPresent()) { parsedArguments.add(child.getValue()); return this.parseCommand(parsedArguments, commandContext, commandQueue, child); - } /*else if (result.getFailure().isPresent() && root.children.size() == 1) { - }*/ + } } } } @@ -201,7 +200,21 @@ public final class CommandTree { getChain(root).stream().map(Node::getValue).collect(Collectors.toList()), stringOrEmpty(commandQueue.peek())); } - /* We have already traversed the tree */ + /* If we couldn't match a child, check if there's a command attached and execute it */ + if (root.getValue() != null && root.getValue().getOwningCommand() != null) { + final Command command = root.getValue().getOwningCommand(); + if (!this.getCommandManager().hasPermission(commandContext.getSender(), + command.getCommandPermission())) { + throw new NoPermissionException(command.getCommandPermission(), + commandContext.getSender(), + this.getChain(root) + .stream() + .map(Node::getValue) + .collect(Collectors.toList())); + } + return Optional.of(root.getValue().getOwningCommand()); + } + /* We know that there's no command and we also cannot match any of the children */ throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() .apply(parsedArguments, root), commandContext.getSender(), this.getChain(root) @@ -235,6 +248,20 @@ public final class CommandTree { } else if (!child.getValue().isRequired()) { return Optional.ofNullable(this.cast(child.getValue().getOwningCommand())); } else if (child.isLeaf()) { + /* The child is not a leaf, but may have an intermediary executor, attempt to use it */ + if (root.getValue() != null && root.getValue().getOwningCommand() != null) { + final Command command = root.getValue().getOwningCommand(); + if (!this.getCommandManager().hasPermission(commandContext.getSender(), + command.getCommandPermission())) { + throw new NoPermissionException(command.getCommandPermission(), + commandContext.getSender(), + this.getChain(root) + .stream() + .map(Node::getValue) + .collect(Collectors.toList())); + } + return Optional.of(command); + } /* Not enough arguments */ throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() .apply(Objects.requireNonNull( @@ -245,13 +272,21 @@ public final class CommandTree { .map(Node::getValue) .collect(Collectors.toList())); } else { - /* - throw new NoSuchCommandException(commandContext.getSender(), - this.getChain(root) - .stream() - .map(Node::getValue) - .collect(Collectors.toList()), - "");*/ + /* The child is not a leaf, but may have an intermediary executor, attempt to use it */ + if (root.getValue() != null && root.getValue().getOwningCommand() != null) { + final Command command = root.getValue().getOwningCommand(); + if (!this.getCommandManager().hasPermission(commandContext.getSender(), + command.getCommandPermission())) { + throw new NoPermissionException(command.getCommandPermission(), + commandContext.getSender(), + this.getChain(root) + .stream() + .map(Node::getValue) + .collect(Collectors.toList())); + } + return Optional.of(command); + } + /* Child does not have a command and so we cannot proceed */ throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() .apply(parsedArguments, root), commandContext.getSender(), this.getChain(root) @@ -357,8 +392,7 @@ public final class CommandTree { final ArgumentParseResult 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) { - }*/ + } } } } @@ -407,6 +441,12 @@ public final class CommandTree { node = tempNode; } if (node.getValue() != null) { + if (node.getValue().getOwningCommand() != null) { + throw new IllegalStateException(String.format( + "Duplicate command chains detected. Node '%s' already has an owning command (%s)", + node.toString(), node.getValue().getOwningCommand().toString() + )); + } node.getValue().setOwningCommand(command); } // Verify the command structure every time we add a new command @@ -474,7 +514,6 @@ public final class CommandTree { // noinspection all final CommandPermission commandPermission = node.getValue().getOwningCommand().getCommandPermission(); /* All leaves must necessarily have an owning command */ - // noinspection all node.nodeMeta.put("permission", commandPermission); // Get chain and order it tail->head then skip the tail (leaf node) List>> chain = this.getChain(node); @@ -483,12 +522,25 @@ public final class CommandTree { // Go through all nodes from the tail upwards until a collision occurs for (final Node> commandArgumentNode : chain) { final CommandPermission existingPermission = (CommandPermission) commandArgumentNode.nodeMeta.get("permission"); + + CommandPermission permission; if (existingPermission != null) { - commandArgumentNode.nodeMeta.put("permission", - OrPermission.of(Arrays.asList(commandPermission, existingPermission))); + permission = OrPermission.of(Arrays.asList(commandPermission, existingPermission)); } else { - commandArgumentNode.nodeMeta.put("permission", commandPermission); + permission = commandPermission; } + + /* Now also check if there's a command handler attached to an upper level node */ + if (commandArgumentNode.getValue() != null && commandArgumentNode.getValue().getOwningCommand() != null) { + final Command command = commandArgumentNode.getValue().getOwningCommand(); + if (this.getCommandManager().getSetting(CommandManager.ManagerSettings.ENFORCE_INTERMEDIARY_PERMISSIONS)) { + permission = command.getCommandPermission(); + } else { + permission = OrPermission.of(Arrays.asList(permission, command.getCommandPermission())); + } + } + + commandArgumentNode.nodeMeta.put("permission", permission); } }); } 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 e0fedb81..4e5f2141 100644 --- a/cloud-core/src/test/java/com/intellectualsites/commands/CommandTreeTest.java +++ b/cloud-core/src/test/java/com/intellectualsites/commands/CommandTreeTest.java @@ -46,6 +46,8 @@ class CommandTreeTest { @BeforeAll static void newTree() { manager = new TestCommandManager(); + + /* Build general test commands */ manager.command(manager.commandBuilder("test", SimpleCommandMeta.empty()) .literal("one").build()) .command(manager.commandBuilder("test", SimpleCommandMeta.empty()) @@ -57,6 +59,8 @@ class CommandTreeTest { .optional("num", EXPECTED_INPUT_NUMBER)) .build()) .command(manager.commandBuilder("req").withSenderType(SpecificCommandSender.class).build()); + + /* Build command to test command proxying */ final Command toProxy = manager.commandBuilder("test") .literal("unproxied") .argument(StringArgument.required("string")) @@ -66,6 +70,17 @@ class CommandTreeTest { .build(); manager.command(toProxy); manager.command(manager.commandBuilder("proxy").proxies(toProxy).build()); + + /* Build command for testing intermediary and final executors */ + manager.command(manager.commandBuilder("command") + .withPermission("command.inner") + .literal("inner") + .handler(c -> System.out.println("Using inner command")) + .build()); + manager.command(manager.commandBuilder("command") + .withPermission("command.outer") + .handler(c -> System.out.println("Using outer command")) + .build()); } @Test @@ -136,6 +151,12 @@ class CommandTreeTest { manager.executeCommand(new TestCommandSender(), "proxy foo 10").join(); } + @Test + void testIntermediary() { + manager.executeCommand(new TestCommandSender(), "command inner").join(); + manager.executeCommand(new TestCommandSender(), "command").join(); + } + public static final class SpecificCommandSender extends TestCommandSender { } diff --git a/cloud-core/src/test/java/com/intellectualsites/commands/TestCommandManager.java b/cloud-core/src/test/java/com/intellectualsites/commands/TestCommandManager.java index 4f204603..8cad0e5f 100644 --- a/cloud-core/src/test/java/com/intellectualsites/commands/TestCommandManager.java +++ b/cloud-core/src/test/java/com/intellectualsites/commands/TestCommandManager.java @@ -47,6 +47,7 @@ public class TestCommandManager extends CommandManager { @Override public final boolean hasPermission(@Nonnull final TestCommandSender sender, @Nonnull final String permission) { + System.out.printf("Testing permission: %s\n", permission); return !permission.equalsIgnoreCase("no"); } diff --git a/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java b/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java index d2c95dd5..34b93098 100644 --- a/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java +++ b/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java @@ -42,6 +42,7 @@ import com.intellectualsites.commands.bukkit.BukkitCommandManager; import com.intellectualsites.commands.bukkit.BukkitCommandMetaBuilder; import com.intellectualsites.commands.bukkit.CloudBukkitCapabilities; import com.intellectualsites.commands.bukkit.parsers.WorldArgument; +import com.intellectualsites.commands.exceptions.InvalidSyntaxException; import com.intellectualsites.commands.execution.AsynchronousCommandExecutionCoordinator; import com.intellectualsites.commands.execution.CommandExecutionCoordinator; import com.intellectualsites.commands.extra.confirmation.CommandConfirmationManager; @@ -92,7 +93,7 @@ public final class BukkitTest extends JavaPlugin { mgr); try { - ((PaperCommandManager) mgr).registerBrigadier(); + mgr.registerBrigadier(); } catch (final Exception e) { getLogger().warning("Failed to initialize Brigadier support: " + e.getMessage()); } @@ -116,7 +117,6 @@ public final class BukkitTest extends JavaPlugin { BukkitCommandMetaBuilder.builder().withDescription(p.get(StandardParameters.DESCRIPTION, "No description")).build()); annotationParser.parse(this); - //noinspection all mgr.command(mgr.commandBuilder("gamemode", this.metaWithDescription("Your ugli"), "gajmöde") .argument(EnumArgument.required(GameMode.class, "gamemode")) @@ -148,6 +148,9 @@ public final class BukkitTest extends JavaPlugin { )); }) .build()) + .command(mgr.commandBuilder("uuidtest") + .handler(c -> c.getSender().sendMessage("Hey yo dum, provide a UUID idiot. Thx!")) + .build()) .command(mgr.commandBuilder("uuidtest") .argument(UUID.class, "uuid", builder -> builder .asRequired() @@ -201,11 +204,14 @@ public final class BukkitTest extends JavaPlugin { .handler(confirmationManager.createConfirmationExecutionHandler()).build()) .command(mgr.commandBuilder("cloud") .literal("help") + .withPermission("cloud.help") .argument(StringArgument.newBuilder("query").greedy() .asOptionalWithDefault("") .build(), Description.of("Help Query")) .handler(c -> minecraftHelp.queryCommands(c.get("query").orElse(""), c.getSender())).build()); + + mgr.registerExceptionHandler(InvalidSyntaxException.class, (c, e) -> e.printStackTrace()); } catch (final Exception e) { e.printStackTrace(); } @@ -222,7 +228,7 @@ public final class BukkitTest extends JavaPlugin { } @Confirmation - @CommandMethod("cloud debug") + @CommandMethod(value = "cloud", permission = "cloud.debug") private void doHelp() { final Set capabilities = this.mgr.queryCapabilities(); Bukkit.broadcastMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "Capabilities"); diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/bukkit/BukkitCommand.java b/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/bukkit/BukkitCommand.java index cd55e004..de8fa009 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/bukkit/BukkitCommand.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/bukkit/BukkitCommand.java @@ -155,4 +155,9 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi return this.cloudCommand.getCommandPermission().toString(); } + @Override + public String getUsage() { + return this.manager.getCommandSyntaxFormatter().apply(this.cloudCommand.getArguments(), null); + } + }