Skip to content

Commit 346b84c

Browse files
feat: add ExecutorWrapper annotation and inherited annotations support (#38)
Co-authored-by: Strokkur24 <strokkur.24@gmail.com>
1 parent 0085c52 commit 346b84c

File tree

34 files changed

+923
-125
lines changed

34 files changed

+923
-125
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* StrokkCommands - A super simple annotation based zero-shade Paper command API library.
3+
* Copyright (C) 2025 Strokkur24
4+
*
5+
* This library is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 2.1 of the License, or (at your option) any later version.
9+
*
10+
* This library is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public
16+
* License along with this library; if not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package net.strokkur.commands;
19+
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.lang.reflect.Method;
25+
26+
/// Declares that an annotation should be treated as a provider for executor wrappers.
27+
///
28+
/// Apply this annotation to your own custom annotation:
29+
/// ```java
30+
/// @CustomExecutorWrapper
31+
/// @interface TimingWrapper {}
32+
/// ```
33+
///
34+
/// ## Annotating the wrapper method
35+
/// The wrapper method must be annotated with your custom wrapper annotation.
36+
/// Generally, the wrapper method should be static, except if only used inside a single
37+
/// command class, in which it may be non-static.
38+
///
39+
/// The wrapper method should have a `Command<S>` parameter, returning a `Command<S>`.
40+
///
41+
/// Additionally, the wrapper method may define an optional, second parameter to retrieve the
42+
/// [Method] instance the wrapper was called for.
43+
///
44+
/// - `Command<S> wrapper(Command<S>)`
45+
/// - `Command<S> wrapper(Command<S>, Method)`
46+
///
47+
/// ## Example usage
48+
/// ```java
49+
/// @TimingWrapper
50+
/// static Command<CommandSourceStack> time(Command<CommandSourceStack> executor) {
51+
/// return (ctx) -> {
52+
/// final long start = System.nanoTime();
53+
/// try {
54+
/// return executor.run(ctx);
55+
/// } finally {
56+
/// LOGGER.info("Took {} nanoseconds!", System.nanoTime() - start);
57+
/// }
58+
/// };
59+
/// }
60+
/// ```
61+
///
62+
/// ## Using the custom executor wrapper annotation
63+
/// Apply your wrapper annotation to classes or methods:
64+
/// ```java
65+
/// @Command("mycommand")
66+
/// @TimingWrapper // Applies to all methods in this command
67+
/// class MyCommand {
68+
///
69+
/// @Executes("fast")
70+
/// void fastCommand(S source) {
71+
/// // Defaults to @TimingWrapper
72+
/// }
73+
///
74+
/// @Executes("admin")
75+
/// @DifferentWrapper // Override with a different wrapper for this method
76+
/// void adminCommand(S source) {
77+
/// // ...
78+
/// }
79+
/// }
80+
/// ```
81+
///
82+
/// @see UnsetExecutorWrapper
83+
@Retention(RetentionPolicy.SOURCE)
84+
@Target(ElementType.ANNOTATION_TYPE)
85+
public @interface CustomExecutorWrapper {
86+
}

annotations-common/src/main/java/net/strokkur/commands/DefaultExecutes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
/// If multiple [DefaultExecutes]-annotated methods are present, the **deeper one** in the tree takes precedence.
6363
/// If multiple [DefaultExecutes]-annotated methods are present on the same path, the first declared one takes precedence.
6464
@Retention(RetentionPolicy.SOURCE)
65-
@Target(ElementType.METHOD)
65+
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
6666
public @interface DefaultExecutes {
6767
/// A literal path to prepend to the method.
6868
///

annotations-common/src/main/java/net/strokkur/commands/Executes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
/// }
3636
/// ```
3737
@Retention(RetentionPolicy.SOURCE)
38-
@Target(ElementType.METHOD)
38+
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
3939
public @interface Executes {
4040
/// A literal path to prepend to the method.
4141
///

annotations-common/src/main/java/net/strokkur/commands/Subcommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
/// }
4444
/// ```
4545
@Retention(RetentionPolicy.SOURCE)
46-
@Target({ElementType.TYPE, ElementType.FIELD})
46+
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
4747
public @interface Subcommand {
4848
/// {@return the literal path to prepend to the nested subcommand}
4949
String value() default "";
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* StrokkCommands - A super simple annotation based zero-shade Paper command API library.
3+
* Copyright (C) 2025 Strokkur24
4+
*
5+
* This library is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 2.1 of the License, or (at your option) any later version.
9+
*
10+
* This library is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public
16+
* License along with this library; if not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package net.strokkur.commands;
19+
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/// Unsets an inherited executor wrapper. This annotation can be used if an entire class is annotated
26+
/// with a [CustomExecutorWrapper]-annotated annotation, but certain children should be excluded from this.
27+
///
28+
/// ## Example usage
29+
/// ```java
30+
/// @TimingsExecutor
31+
/// @Command("heavy-command")
32+
/// class HeavyCommand {
33+
///
34+
/// @Executes
35+
/// void someHeavyOperation() {
36+
/// // This is wrapped by `@TimingsExecutor`
37+
/// }
38+
///
39+
/// @Executes("light")
40+
/// @UnsetExecutorWrapper
41+
/// void someLightOperation() {
42+
/// // This handler is *not* wrapped by `@TimingsExecutor`
43+
/// }
44+
/// }
45+
/// ```
46+
///
47+
/// @see CustomExecutorWrapper
48+
@Retention(RetentionPolicy.SOURCE)
49+
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
50+
public @interface UnsetExecutorWrapper {
51+
}

processor-common/src/main/java/net/strokkur/commands/internal/NodeUtils.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
package net.strokkur.commands.internal;
1919

2020
import net.strokkur.commands.Literal;
21+
import net.strokkur.commands.UnsetExecutorWrapper;
2122
import net.strokkur.commands.internal.abstraction.AnnotationsHolder;
2223
import net.strokkur.commands.internal.abstraction.SourceClass;
24+
import net.strokkur.commands.internal.abstraction.SourceElement;
2325
import net.strokkur.commands.internal.abstraction.SourceVariable;
2426
import net.strokkur.commands.internal.arguments.BrigadierArgumentConverter;
2527
import net.strokkur.commands.internal.arguments.BrigadierArgumentType;
@@ -31,9 +33,11 @@
3133
import net.strokkur.commands.internal.exceptions.ConversionException;
3234
import net.strokkur.commands.internal.intermediate.attributes.Attributable;
3335
import net.strokkur.commands.internal.intermediate.attributes.AttributeKey;
36+
import net.strokkur.commands.internal.intermediate.registrable.ExecutorWrapperRegistry;
3437
import net.strokkur.commands.internal.intermediate.registrable.RegistrableRegistry;
3538
import net.strokkur.commands.internal.intermediate.registrable.RequirementRegistry;
3639
import net.strokkur.commands.internal.intermediate.registrable.SuggestionsRegistry;
40+
import net.strokkur.commands.internal.intermediate.tree.CommandNode;
3741
import net.strokkur.commands.internal.util.ForwardingMessagerWrapper;
3842
import net.strokkur.commands.internal.util.MessagerWrapper;
3943

@@ -47,9 +51,25 @@ public record NodeUtils(
4751
MessagerWrapper messager,
4852
BrigadierArgumentConverter converter,
4953
SuggestionsRegistry suggestionsRegistry,
50-
RequirementRegistry requirementRegistry
54+
RequirementRegistry requirementRegistry,
55+
ExecutorWrapperRegistry executorWrapperRegistry
5156
) implements ForwardingMessagerWrapper {
5257

58+
public void applyExecutorTransform(final Attributable node, final AnnotationsHolder element) {
59+
if (element.hasAnnotationInherited(UnsetExecutorWrapper.class)) {
60+
node.setAttribute(AttributeKey.EXECUTOR_WRAPPER_UNSET, true);
61+
return;
62+
}
63+
64+
this.applyRegistrableProvider(
65+
node,
66+
element,
67+
this.executorWrapperRegistry(),
68+
AttributeKey.EXECUTOR_WRAPPER,
69+
"executor wrapper"
70+
);
71+
}
72+
5373
public List<CommandArgument> parseArguments(final List<? extends SourceVariable> variables) {
5474
final List<CommandArgument> arguments = new ArrayList<>(variables.size());
5575

@@ -87,20 +107,20 @@ public List<CommandArgument> parseArguments(final List<? extends SourceVariable>
87107
}
88108

89109
public <T> void applyRegistrableProvider(
90-
final Attributable argument,
91-
final AnnotationsHolder parameter,
110+
final Attributable attributable,
111+
final AnnotationsHolder element,
92112
final RegistrableRegistry<T> registry,
93113
final AttributeKey<T> key,
94114
final String name
95115
) {
96116
boolean found = false;
97-
for (final SourceClass annotationType : parameter.getAllAnnotations()) {
117+
for (final SourceClass annotationType : element.getAllAnnotations()) {
98118
final Optional<T> provider = registry.getProvider(annotationType);
99119
if (provider.isPresent()) {
100120
if (found) {
101-
this.infoSource("Multiple %s providers has been declared", parameter, name);
121+
this.infoSource("Multiple %s providers has been declared", element, name);
102122
} else {
103-
argument.setAttribute(key, provider.get());
123+
attributable.setAttribute(key, provider.get());
104124
found = true;
105125
}
106126
}

processor-common/src/main/java/net/strokkur/commands/internal/StrokkCommandsProcessor.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package net.strokkur.commands.internal;
1919

20+
import net.strokkur.commands.CustomExecutorWrapper;
2021
import net.strokkur.commands.CustomRequirement;
2122
import net.strokkur.commands.CustomSuggestion;
2223
import net.strokkur.commands.internal.abstraction.SourceClass;
@@ -26,6 +27,7 @@
2627
import net.strokkur.commands.internal.arguments.BrigadierArgumentConverter;
2728
import net.strokkur.commands.internal.exceptions.ProviderAlreadyRegisteredException;
2829
import net.strokkur.commands.internal.intermediate.CommonTreePostProcessor;
30+
import net.strokkur.commands.internal.intermediate.registrable.ExecutorWrapperRegistry;
2931
import net.strokkur.commands.internal.intermediate.registrable.RegistrableRegistry;
3032
import net.strokkur.commands.internal.intermediate.registrable.RequirementRegistry;
3133
import net.strokkur.commands.internal.intermediate.registrable.SuggestionsRegistry;
@@ -92,8 +94,9 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
9294
final MessagerWrapper messagerWrapper = MessagerWrapper.wrap(super.processingEnv.getMessager());
9395
final SuggestionsRegistry suggestionsRegistry = createAndFillRegistry(CustomSuggestion.class, SuggestionsRegistry::new, roundEnv, messagerWrapper);
9496
final RequirementRegistry requirementRegistry = createAndFillRegistry(CustomRequirement.class, RequirementRegistry::new, roundEnv, messagerWrapper);
97+
final ExecutorWrapperRegistry executorWrapperRegistry = createAndFillRegistry(CustomExecutorWrapper.class, ExecutorWrapperRegistry::new, roundEnv, messagerWrapper);
9598

96-
final NodeUtils nodeUtils = new NodeUtils(getPlatformUtils(), messagerWrapper, getConverter(messagerWrapper), suggestionsRegistry, requirementRegistry);
99+
final NodeUtils nodeUtils = new NodeUtils(getPlatformUtils(), messagerWrapper, getConverter(messagerWrapper), suggestionsRegistry, requirementRegistry, executorWrapperRegistry);
97100
final CommandParser parser = new CommandParserImpl(
98101
messagerWrapper,
99102
nodeUtils,
@@ -168,7 +171,7 @@ private void processElement(
168171
boolean debug = System.getProperty(MessagerWrapper.DEBUG_SYSTEM_PROPERTY) != null;
169172

170173
final C commandInformation = getCommandInformation(sourceClass);
171-
final CommandNode commandTree = parser.createCommandTree(getCommandName(sourceClass.getAnnotationElseThrow(targetAnnotationClass())), sourceClass);
174+
final CommandNode commandTree = parser.createCommandTree(getCommandName(sourceClass.getAnnotationInheritedElseThrow(targetAnnotationClass())), sourceClass);
172175
if (commandTree == null) {
173176
return;
174177
}
@@ -212,15 +215,15 @@ private <T extends RegistrableRegistry<?>> T createAndFillRegistry(
212215
for (final Element element : roundEnv.getElementsAnnotatedWith(annotationClass)) {
213216
try {
214217
if (element.getKind() != ElementKind.ANNOTATION_TYPE || !(element instanceof TypeElement typeElement)) {
215-
messager.errorElement("non-annotation type annotated with @CustomSuggestion", element);
218+
messager.errorElement("non-annotation type annotated with @" + annotationClass.getSimpleName(), element);
216219
continue;
217220
}
218221

219-
for (final Element annotatedElements : roundEnv.getElementsAnnotatedWith(typeElement)) {
222+
for (final Element annotatedElement : roundEnv.getElementsAnnotatedWith(typeElement)) {
220223
if (registry.tryRegisterProvider(
221224
messager,
222225
new SourceClassImpl(this.processingEnv, (DeclaredType) typeElement.asType()),
223-
SourceTypeUtils.getSourceElement(this.processingEnv, annotatedElements)
226+
SourceTypeUtils.getSourceElement(this.processingEnv, annotatedElement)
224227
)) {
225228
break;
226229
}

processor-common/src/main/java/net/strokkur/commands/internal/abstraction/AnnotationsHolder.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ public interface AnnotationsHolder extends SourceElement {
3030

3131
List<SourceClass> getAllAnnotations();
3232

33-
default boolean hasAnnotation(final Class<? extends Annotation> type) {
34-
return getAnnotationOptional(type).isPresent();
33+
default boolean hasAnnotationInherited(final Class<? extends Annotation> type) {
34+
return this.getAnnotationInheritedOptional(type).isPresent();
3535
}
3636

3737
default <T extends Annotation> @Nullable SourceClass getAnnotationSourceClassField(Class<T> type, String fieldName) throws UnsupportedOperationException {
@@ -42,7 +42,23 @@ default <T extends Annotation> Optional<T> getAnnotationOptional(final Class<T>
4242
return Optional.ofNullable(getAnnotation(type));
4343
}
4444

45-
default <T extends Annotation> T getAnnotationElseThrow(final Class<T> type) throws NoSuchElementException {
46-
return getAnnotationOptional(type).orElseThrow(() -> new NoSuchElementException("No annotation of type " + type.getSimpleName() + " is present."));
45+
default <T extends Annotation> Optional<T> getAnnotationInheritedOptional(final Class<T> type) {
46+
final T direct = this.getAnnotation(type);
47+
if (direct != null) {
48+
return Optional.of(direct);
49+
}
50+
51+
for (final SourceClass annotationClass : this.getAllAnnotations()) {
52+
final T inherited = annotationClass.getAnnotation(type);
53+
if (inherited != null) {
54+
return Optional.of(inherited);
55+
}
56+
}
57+
58+
return Optional.empty();
59+
}
60+
61+
default <T extends Annotation> T getAnnotationInheritedElseThrow(final Class<T> type) throws NoSuchElementException {
62+
return getAnnotationInheritedOptional(type).orElseThrow(() -> new NoSuchElementException("No annotation of type " + type.getSimpleName() + " is present."));
4763
}
4864
}

processor-common/src/main/java/net/strokkur/commands/internal/intermediate/attributes/Attributable.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package net.strokkur.commands.internal.intermediate.attributes;
1919

20+
import org.jetbrains.annotations.Contract;
2021
import org.jspecify.annotations.Nullable;
2122

2223
import java.util.Objects;
@@ -30,7 +31,8 @@ public interface Attributable {
3031
@Nullable
3132
<T> T getAttribute(AttributeKey<T> key);
3233

33-
default <T> T getAttributeOr(AttributeKey<T> key, T defaultValue) {
34+
@Contract("_, !null -> !null")
35+
default <T> @Nullable T getAttributeOr(AttributeKey<T> key, @Nullable T defaultValue) {
3436
return Optional.ofNullable(getAttribute(key)).orElse(defaultValue);
3537
}
3638

processor-common/src/main/java/net/strokkur/commands/internal/intermediate/attributes/AttributeKey.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package net.strokkur.commands.internal.intermediate.attributes;
1919

2020
import net.strokkur.commands.internal.intermediate.access.ExecuteAccess;
21+
import net.strokkur.commands.internal.intermediate.registrable.ExecutorWrapperProvider;
2122
import net.strokkur.commands.internal.intermediate.registrable.RequirementProvider;
2223
import net.strokkur.commands.internal.intermediate.registrable.SuggestionProvider;
2324
import org.jetbrains.annotations.Contract;
@@ -32,6 +33,9 @@ public interface AttributeKey<T> {
3233
AttributeKey<DefaultExecutable> DEFAULT_EXECUTABLE = create("default_executable", null);
3334
AttributeKey<List<ExecuteAccess<?>>> ACCESS_STACK = create("access_stack", null);
3435

36+
AttributeKey<ExecutorWrapperProvider> EXECUTOR_WRAPPER = create("executor_wrapper", null);
37+
AttributeKey<Boolean> EXECUTOR_WRAPPER_UNSET = create("executor_wrapper_unset", false);
38+
3539
AttributeKey<RequirementProvider> REQUIREMENT_PROVIDER = create("requirement_provider", null);
3640
AttributeKey<SuggestionProvider> SUGGESTION_PROVIDER = create("suggestion_provider", null);
3741

0 commit comments

Comments
 (0)