Skip to content

Commit

Permalink
feat(annotations): add command containers (#364)
Browse files Browse the repository at this point in the history
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.).
  • Loading branch information
Citymonstret committed Jun 3, 2022
1 parent 32198cf commit ab849ff
Show file tree
Hide file tree
Showing 15 changed files with 543 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Core: Add delegating command execution handlers ([#363](https://github.com/Incendo/cloud/pull/363))
- Core: Add `builder()` getter to `Command.Builder` ([#363](https://github.com/Incendo/cloud/pull/363))
- Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353))
- Annotations: `@CommandContainer` annotation processing

### Fixed
- Core: Fix missing caption registration for the regex caption ([#351](https://github.com/Incendo/cloud/pull/351))
Expand Down
2 changes: 2 additions & 0 deletions cloud-annotations/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ plugins {

dependencies {
implementation(projects.cloudCore)

testImplementation(libs.compileTesting)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import cloud.commandframework.annotations.injection.RawArgs;
import cloud.commandframework.annotations.parsers.MethodArgumentParser;
import cloud.commandframework.annotations.parsers.Parser;
import cloud.commandframework.annotations.processing.CommandContainerProcessor;
import cloud.commandframework.annotations.specifier.Completions;
import cloud.commandframework.annotations.suggestions.MethodSuggestionsProvider;
import cloud.commandframework.annotations.suggestions.Suggestions;
Expand All @@ -48,16 +49,21 @@
import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.meta.SimpleCommandMeta;
import io.leangen.geantyref.TypeToken;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -66,6 +72,8 @@
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

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

/**
* Parses all known {@link cloud.commandframework.annotations.processing.CommandContainer command containers}.
*
* @return Collection of parsed commands
* @throws Exception re-throws all encountered exceptions.
* @since 1.7.0
* @see cloud.commandframework.annotations.processing.CommandContainer CommandContainer for more information.
*/
public @NonNull Collection<@NonNull Command<C>> parseContainers() throws Exception {
final List<Command<C>> commands = new LinkedList<>();

final List<String> classes;
try (InputStream stream = this.getClass().getClassLoader().getResourceAsStream(CommandContainerProcessor.PATH)) {
if (stream == null) {
return Collections.emptyList();
}

try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
classes = reader.lines().distinct().collect(Collectors.toList());
}
}

for (final String className : classes) {
final Class<?> commandContainer = Class.forName(className);

// We now have the class, and we now just need to decide what constructor to invoke.
// We first try to find a constructor which takes in the parser.
@MonotonicNonNull Object instance;
try {
instance = commandContainer.getConstructor(AnnotationParser.class).newInstance(this);
} catch (final NoSuchMethodException ignored) {
try {
// Then we try to find a no-arg constructor.
instance = commandContainer.getConstructor().newInstance();
} catch (final NoSuchMethodException e) {
// If neither are found, we panic!
throw new IllegalStateException(
String.format(
"Command container %s has no valid constructors",
commandContainer
),
e
);
}
}
commands.addAll(this.parse(instance));
}

return Collections.unmodifiableList(commands);
}

/**
* Scan a class instance of {@link CommandMethod} annotations and attempt to
* compile them into {@link Command} instances
* compile them into {@link Command} instances.
*
* @param instance Instance to scan
* @param <T> Type of the instance
* @return Collection of parsed annotations
* @return Collection of parsed commands
*/
@SuppressWarnings({"deprecation", "unchecked", "rawtypes"})
public <T> @NonNull Collection<@NonNull Command<C>> parse(final @NonNull T instance) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// MIT License
//
// Copyright (c) 2021 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.annotations.processing;

import cloud.commandframework.annotations.AnnotationParser;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicates that the class contains
* {@link cloud.commandframework.annotations.CommandMethod command metods}.
* <p>
* If using <i>cloud-annotations</i> as an annotation processor, then the class will
* be listed in a special file under META-INF. These containers can be collectively
* parsed using {@link AnnotationParser#parseContainers()}, which will create instances
* of the containers and then call {@link AnnotationParser#parse(Object)} with the created instance.
* <p>
* Every class annotated with {@link CommandContainer} needs to be {@code public}, and it
* also needs to have one of the following:
* <ul>
* <li>A {@code public} no-arg constructor</li>
* <li>A {@code public} constructor with {@link AnnotationParser} as the sole parameter</li>
* </ul>
* <p>
* <b>NOTE:</b> For container parsing to work, you need to make sure that <i>cloud-annotations</i> is added
* as an annotation processor.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CommandContainer {
String ANNOTATION_PATH = "cloud.commandframework.annotations.processing.CommandContainer";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// MIT License
//
// Copyright (c) 2021 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.annotations.processing;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.StandardLocation;
import org.checkerframework.checker.nullness.qual.NonNull;

@SupportedAnnotationTypes(CommandContainer.ANNOTATION_PATH)
public final class CommandContainerProcessor extends AbstractProcessor {

/**
* The file in which all command container names are stored.
*/
public static final String PATH = "META-INF/commands/cloud.commandframework.annotations.processing.CommandContainer";

@Override
public boolean process(
final @NonNull Set<? extends TypeElement> annotations,
final @NonNull RoundEnvironment roundEnv
) {
final List<String> validTypes = new ArrayList<>();

final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(CommandContainer.class);
if (elements.isEmpty()) {
return false; // Nothing to process...
}

for (final Element element : elements) {
if (element.getKind() != ElementKind.CLASS) {
this.processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
String.format(
"@CommandMethod found on unsupported element type '%s' (%s)",
element.getKind().name(),
element.getSimpleName().toString()
),
element
);
return false;
}

element.accept(new CommandContainerVisitor(this.processingEnv, validTypes), null);
}

for (final String type : validTypes) {
this.processingEnv.getMessager().printMessage(
Diagnostic.Kind.NOTE,
String.format(
"Found valid @CommandMethod annotated class: %s",
type
)
);
}
this.writeCommandFile(validTypes);

// https://errorprone.info/bugpattern/DoNotClaimAnnotations
return false;
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}

@SuppressWarnings({"unused", "try"})
private void writeCommandFile(final @NonNull List<String> types) {
try (BufferedWriter writer = new BufferedWriter(this.processingEnv.getFiler().createResource(
StandardLocation.CLASS_OUTPUT,
"",
PATH
).openWriter())) {
for (final String t : types) {
writer.write(t);
writer.newLine();
}
writer.flush();
} catch (final IOException e) {
e.printStackTrace();
}
}
}
Loading

0 comments on commit ab849ff

Please sign in to comment.