diff --git a/common/src/main/java/revxrsal/commands/annotation/DefaultFor.java b/common/src/main/java/revxrsal/commands/annotation/DefaultFor.java index 32d8fffa..02ec29a8 100644 --- a/common/src/main/java/revxrsal/commands/annotation/DefaultFor.java +++ b/common/src/main/java/revxrsal/commands/annotation/DefaultFor.java @@ -23,6 +23,25 @@ * } * }} * + *

+ * To avoid having to specify all parent paths explicitly, {@link DefaultFor} + * allows using the special character {@literal ~} (tilde) as a placeholder for all + * parent paths. + *

+ * This example demonstrates this: + *

{@code
+ * @Command("foo", "bar", "buzz")
+ * public class TestCommand {
+ *
+ *     // This would execute for the following:
+ *     // - /foo, /foo help
+ *     // - /bar, /bar help
+ *     // - /buzz, /buzz help
+ *     @DefaultFor({"~", "~ help"})
+ *     public void help(@Default("1") int page) {
+ *         ...
+ *     }
+ * }}
*/ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) diff --git a/common/src/main/java/revxrsal/commands/core/CommandParser.java b/common/src/main/java/revxrsal/commands/core/CommandParser.java index 91806bb9..ad089ee9 100644 --- a/common/src/main/java/revxrsal/commands/core/CommandParser.java +++ b/common/src/main/java/revxrsal/commands/core/CommandParser.java @@ -53,7 +53,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; import static java.util.Collections.addAll; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toMap; @@ -66,6 +69,18 @@ */ final class CommandParser { + /** + * The special character that gets parsed by {@link DefaultFor} + * to be replaced with parent paths. + */ + private static final char INHERIT_PARENT_PATH = '~'; + + /** + * The special character that gets parsed by {@link DefaultFor} + * to be replaced with parent paths. + */ + private static final String S_INHERIT_PARENT_PATH = "~"; + /** * Handles the response returned by (void) methods */ @@ -139,9 +154,8 @@ public static void parse(@NotNull BaseCommandHandler handler, @NotNull Class /* Check if the command is default, and if so, generate a path for it */ String[] defPaths = reader.get(DefaultFor.class, DefaultFor::value); - List defaultPaths = defPaths == null ? emptyList() : Arrays.stream(defPaths) - .map(CommandPath::parse) - .collect(Collectors.toList()); + List defaultPaths = defPaths == null ? emptyList() : + parseDefaultPaths(defPaths, container, method, reader); boolean isDefault = !defaultPaths.isEmpty(); /* Generate categories for default paths if not created already */ @@ -370,7 +384,7 @@ private static List getParameters(@NotNull BaseCommandHandler BaseCommandParameter param = new BaseCommandParameter( paramAnns.get(Description.class, Description::value), i, - defaultValue == null ? emptyList() : Collections.unmodifiableList(Arrays.asList(defaultValue)), + defaultValue == null ? emptyList() : Collections.unmodifiableList(asList(defaultValue)), i == methodParameters.length - 1 && !paramAnns.contains(Single.class), paramAnns.contains(Optional.class) || paramAnns.contains(Default.class), command, @@ -432,8 +446,8 @@ private static List getParameters(@NotNull BaseCommandHandler /** * Concatenates all the paths for a command by merging those - * defined by {@link Command}, {@link Subcommand}, and those - * defined in the parent class, and any other parent classes + * defined by {@link Command}, {@link Subcommand}, {@link DefaultFor}, and + * those defined in the parent class, and any other parent classes. * * @param container The class containing the commands * @param method The method that contains annotations @@ -443,16 +457,55 @@ private static List getParameters(@NotNull BaseCommandHandler private static List getCommandPath(@NotNull Class container, @NotNull Method method, @NotNull AnnotationReader reader) { - List paths = new ArrayList<>(); DefaultFor defaultFor = reader.get(DefaultFor.class); if (defaultFor != null) { - return Arrays.stream(defaultFor.value()).map(CommandPath::parse) - .collect(Collectors.toList()); + return parseDefaultPaths(defaultFor.value(), container, method, reader); } + return parseCommandAnnotations(container, method, reader); + } + + /** + * Parses the paths in {@link DefaultFor}. This will automatically replace + * {@code ~} with the top-level parent paths. + * + * @param path The path, as defined in {@link DefaultFor#value()} + * @param container The container class + * @param method The method that contains the annotation + * @param reader The annotation reader + * @return The parsed CommandPaths. + */ + @NotNull + private static List parseDefaultPaths( + String[] path, + @NotNull Class container, + @NotNull Method method, + @NotNull AnnotationReader reader + ) { + return stream(path) + .flatMap(defaultPath -> { + if (defaultPath.indexOf(INHERIT_PARENT_PATH) != -1) { + return parseCommandAnnotations(container, method, reader) + .stream() + .map(CommandPath::toRealString) + .map(v -> defaultPath.replace(S_INHERIT_PARENT_PATH, v)) + .map(CommandPath::parse); + } else + return Stream.of(CommandPath.parse(defaultPath)); + }) + .collect(Collectors.toList()); + } + + private static List parseCommandAnnotations( + @NotNull Class container, + @NotNull Method method, + @NotNull AnnotationReader reader + ) { + List paths = new ArrayList<>(); List commands = new ArrayList<>(); List subcommands = new ArrayList<>(); + Command commandAnnotation = reader.get(Command.class, "Method " + method.getName() + " does not have a parent command! You might have forgotten one of the following:\n" + "- @Command on the method or class\n" + "- implement OrphanCommand"); @@ -511,5 +564,4 @@ private static void putOrError(Map map, K key, V value, String err) } map.put(key, value); } - }