Skip to content

Commit 8a7dcd2

Browse files
authored
feat(annotations): add command containers (#364)
This is the first part of the introduction of annotation processing to cloud. A new `@CommandContainer` annotation has been introduced, which can be placed on classes to have the annotation parser automatically construct & parse the classes when `AnnotationParser.parseContainers()` is invoked. A future PR will introduce another processor that will scan for `@CommandMethod` annotations and verify the integrity of the annotated methods (visibility, argument annotations, etc.).
1 parent bcc9d30 commit 8a7dcd2

File tree

15 files changed

+543
-2
lines changed

15 files changed

+543
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Core: Add delegating command execution handlers ([#363](https://github.com/Incendo/cloud/pull/363))
1313
- Core: Add `builder()` getter to `Command.Builder` ([#363](https://github.com/Incendo/cloud/pull/363))
1414
- Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353))
15+
- Annotations: `@CommandContainer` annotation processing
1516

1617
### Fixed
1718
- Core: Fix missing caption registration for the regex caption ([#351](https://github.com/Incendo/cloud/pull/351))

cloud-annotations/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ plugins {
55

66
dependencies {
77
implementation(projects.cloudCore)
8+
9+
testImplementation(libs.compileTesting)
810
}

cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import cloud.commandframework.annotations.injection.RawArgs;
3131
import cloud.commandframework.annotations.parsers.MethodArgumentParser;
3232
import cloud.commandframework.annotations.parsers.Parser;
33+
import cloud.commandframework.annotations.processing.CommandContainerProcessor;
3334
import cloud.commandframework.annotations.specifier.Completions;
3435
import cloud.commandframework.annotations.suggestions.MethodSuggestionsProvider;
3536
import cloud.commandframework.annotations.suggestions.Suggestions;
@@ -48,16 +49,21 @@
4849
import cloud.commandframework.meta.CommandMeta;
4950
import cloud.commandframework.meta.SimpleCommandMeta;
5051
import io.leangen.geantyref.TypeToken;
52+
import java.io.BufferedReader;
53+
import java.io.InputStream;
54+
import java.io.InputStreamReader;
5155
import java.lang.annotation.Annotation;
5256
import java.lang.reflect.Method;
5357
import java.lang.reflect.Modifier;
5458
import java.lang.reflect.Parameter;
59+
import java.nio.charset.StandardCharsets;
5560
import java.util.ArrayList;
5661
import java.util.Arrays;
5762
import java.util.Collection;
5863
import java.util.Collections;
5964
import java.util.HashMap;
6065
import java.util.HashSet;
66+
import java.util.LinkedList;
6167
import java.util.List;
6268
import java.util.Map;
6369
import java.util.Optional;
@@ -66,6 +72,8 @@
6672
import java.util.function.BiFunction;
6773
import java.util.function.Function;
6874
import java.util.function.Predicate;
75+
import java.util.stream.Collectors;
76+
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
6977
import org.checkerframework.checker.nullness.qual.NonNull;
7078
import org.checkerframework.checker.nullness.qual.Nullable;
7179

@@ -318,13 +326,64 @@ public void stringProcessor(final @NonNull StringProcessor stringProcessor) {
318326
return Arrays.stream(strings).map(this::processString).toArray(String[]::new);
319327
}
320328

329+
/**
330+
* Parses all known {@link cloud.commandframework.annotations.processing.CommandContainer command containers}.
331+
*
332+
* @return Collection of parsed commands
333+
* @throws Exception re-throws all encountered exceptions.
334+
* @since 1.7.0
335+
* @see cloud.commandframework.annotations.processing.CommandContainer CommandContainer for more information.
336+
*/
337+
public @NonNull Collection<@NonNull Command<C>> parseContainers() throws Exception {
338+
final List<Command<C>> commands = new LinkedList<>();
339+
340+
final List<String> classes;
341+
try (InputStream stream = this.getClass().getClassLoader().getResourceAsStream(CommandContainerProcessor.PATH)) {
342+
if (stream == null) {
343+
return Collections.emptyList();
344+
}
345+
346+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
347+
classes = reader.lines().distinct().collect(Collectors.toList());
348+
}
349+
}
350+
351+
for (final String className : classes) {
352+
final Class<?> commandContainer = Class.forName(className);
353+
354+
// We now have the class, and we now just need to decide what constructor to invoke.
355+
// We first try to find a constructor which takes in the parser.
356+
@MonotonicNonNull Object instance;
357+
try {
358+
instance = commandContainer.getConstructor(AnnotationParser.class).newInstance(this);
359+
} catch (final NoSuchMethodException ignored) {
360+
try {
361+
// Then we try to find a no-arg constructor.
362+
instance = commandContainer.getConstructor().newInstance();
363+
} catch (final NoSuchMethodException e) {
364+
// If neither are found, we panic!
365+
throw new IllegalStateException(
366+
String.format(
367+
"Command container %s has no valid constructors",
368+
commandContainer
369+
),
370+
e
371+
);
372+
}
373+
}
374+
commands.addAll(this.parse(instance));
375+
}
376+
377+
return Collections.unmodifiableList(commands);
378+
}
379+
321380
/**
322381
* Scan a class instance of {@link CommandMethod} annotations and attempt to
323-
* compile them into {@link Command} instances
382+
* compile them into {@link Command} instances.
324383
*
325384
* @param instance Instance to scan
326385
* @param <T> Type of the instance
327-
* @return Collection of parsed annotations
386+
* @return Collection of parsed commands
328387
*/
329388
@SuppressWarnings({"deprecation", "unchecked", "rawtypes"})
330389
public <T> @NonNull Collection<@NonNull Command<C>> parse(final @NonNull T instance) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2021 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.annotations.processing;
25+
26+
import cloud.commandframework.annotations.AnnotationParser;
27+
import java.lang.annotation.ElementType;
28+
import java.lang.annotation.Retention;
29+
import java.lang.annotation.RetentionPolicy;
30+
import java.lang.annotation.Target;
31+
32+
/**
33+
* Indicates that the class contains
34+
* {@link cloud.commandframework.annotations.CommandMethod command metods}.
35+
* <p>
36+
* If using <i>cloud-annotations</i> as an annotation processor, then the class will
37+
* be listed in a special file under META-INF. These containers can be collectively
38+
* parsed using {@link AnnotationParser#parseContainers()}, which will create instances
39+
* of the containers and then call {@link AnnotationParser#parse(Object)} with the created instance.
40+
* <p>
41+
* Every class annotated with {@link CommandContainer} needs to be {@code public}, and it
42+
* also needs to have one of the following:
43+
* <ul>
44+
* <li>A {@code public} no-arg constructor</li>
45+
* <li>A {@code public} constructor with {@link AnnotationParser} as the sole parameter</li>
46+
* </ul>
47+
* <p>
48+
* <b>NOTE:</b> For container parsing to work, you need to make sure that <i>cloud-annotations</i> is added
49+
* as an annotation processor.
50+
*/
51+
@Target(ElementType.TYPE)
52+
@Retention(RetentionPolicy.RUNTIME)
53+
public @interface CommandContainer {
54+
String ANNOTATION_PATH = "cloud.commandframework.annotations.processing.CommandContainer";
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2021 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.annotations.processing;
25+
26+
import java.io.BufferedWriter;
27+
import java.io.IOException;
28+
import java.util.ArrayList;
29+
import java.util.List;
30+
import java.util.Set;
31+
import javax.annotation.processing.AbstractProcessor;
32+
import javax.annotation.processing.RoundEnvironment;
33+
import javax.annotation.processing.SupportedAnnotationTypes;
34+
import javax.lang.model.SourceVersion;
35+
import javax.lang.model.element.Element;
36+
import javax.lang.model.element.ElementKind;
37+
import javax.lang.model.element.TypeElement;
38+
import javax.tools.Diagnostic;
39+
import javax.tools.StandardLocation;
40+
import org.checkerframework.checker.nullness.qual.NonNull;
41+
42+
@SupportedAnnotationTypes(CommandContainer.ANNOTATION_PATH)
43+
public final class CommandContainerProcessor extends AbstractProcessor {
44+
45+
/**
46+
* The file in which all command container names are stored.
47+
*/
48+
public static final String PATH = "META-INF/commands/cloud.commandframework.annotations.processing.CommandContainer";
49+
50+
@Override
51+
public boolean process(
52+
final @NonNull Set<? extends TypeElement> annotations,
53+
final @NonNull RoundEnvironment roundEnv
54+
) {
55+
final List<String> validTypes = new ArrayList<>();
56+
57+
final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(CommandContainer.class);
58+
if (elements.isEmpty()) {
59+
return false; // Nothing to process...
60+
}
61+
62+
for (final Element element : elements) {
63+
if (element.getKind() != ElementKind.CLASS) {
64+
this.processingEnv.getMessager().printMessage(
65+
Diagnostic.Kind.ERROR,
66+
String.format(
67+
"@CommandMethod found on unsupported element type '%s' (%s)",
68+
element.getKind().name(),
69+
element.getSimpleName().toString()
70+
),
71+
element
72+
);
73+
return false;
74+
}
75+
76+
element.accept(new CommandContainerVisitor(this.processingEnv, validTypes), null);
77+
}
78+
79+
for (final String type : validTypes) {
80+
this.processingEnv.getMessager().printMessage(
81+
Diagnostic.Kind.NOTE,
82+
String.format(
83+
"Found valid @CommandMethod annotated class: %s",
84+
type
85+
)
86+
);
87+
}
88+
this.writeCommandFile(validTypes);
89+
90+
// https://errorprone.info/bugpattern/DoNotClaimAnnotations
91+
return false;
92+
}
93+
94+
@Override
95+
public SourceVersion getSupportedSourceVersion() {
96+
return SourceVersion.latest();
97+
}
98+
99+
@SuppressWarnings({"unused", "try"})
100+
private void writeCommandFile(final @NonNull List<String> types) {
101+
try (BufferedWriter writer = new BufferedWriter(this.processingEnv.getFiler().createResource(
102+
StandardLocation.CLASS_OUTPUT,
103+
"",
104+
PATH
105+
).openWriter())) {
106+
for (final String t : types) {
107+
writer.write(t);
108+
writer.newLine();
109+
}
110+
writer.flush();
111+
} catch (final IOException e) {
112+
e.printStackTrace();
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)