Skip to content

Commit

Permalink
feat(api)!: add plugin bootstrap (#265)
Browse files Browse the repository at this point in the history
 * Add ChameleonPluginBoostrap

 * Remove use of KyoriPowered/blossom

 * Tidy up some Javadoc

 * Make all platform Chameleon #create methods return their
   corresponding bootstrap class directly, allowing for
   potential platform-specific methods in the future.

 * Rename Chameleon#getDataFolder to
   Chameleon#getDataDirectory to align with platforms

 * Update Luis' username (SLLCoding -> LooFifteen)

 * Update example to use another package and use correct relocations

 * Update example to show dispatching custom events in the main class

BREAKING CHANGE: Platform Chameleon #create methods no longer accept
the plugin class, instead a plugin bootstrap is required.
The easiest plugin bootstrap implementation would be MyPlugin::new.
  • Loading branch information
joshuasing authored Sep 2, 2023
1 parent c58c887 commit f6e5b38
Show file tree
Hide file tree
Showing 47 changed files with 601 additions and 447 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ If you have spotted a problem or mistake in someone else's pull request, feel fr
## Supporting the authors
If you wish to support this project in a way other than reporting bugs, suggesting ideas, or contributing code, the authors accept donations via GitHub Sponsors and Ko-fi, these donations go towards allowing the authors to spend more time working on this project, or paying for infrastructure for the author's personal projects.
- [Joshua (joshuasing)](https://github.com/sponsors/joshuasing)
- [Luis (SLLCoding)](https://ko-fi.com/SLLCoding)
- [Luis (LooFifteen)](https://ko-fi.com/loofifteen)
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ address security vulnerabilities that affect the most recent version of this fra

| Version | Supported |
|---------------------|--------------------|
| `0.16.0-SNAPSHOT` | :white_check_mark: |
| < `0.16.0-SNAPSHOT` | :x: |
| `0.17.0-SNAPSHOT` | :white_check_mark: |
| < `0.17.0-SNAPSHOT` | :x: |

### Reporting a Vulnerability

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package dev.hypera.chameleon.annotations;

import dev.hypera.chameleon.ChameleonPluginBootstrap;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
Expand Down Expand Up @@ -58,7 +59,7 @@
@NotNull String version();

/**
* The plugin's description, generally a short explaination of what the plugin is used for.
* The plugin's description, generally a short explanation of what the plugin is used for.
*
* @return the plugin's description, or an empty string.
*/
Expand Down Expand Up @@ -94,4 +95,13 @@
*/
@NotNull String[] platforms() default {};

/**
* Returns the plugin bootstrap to use when bootstrapping Chameleon.
* <p><strong>If this is not provided, the annotated class must have a public constructor with a
* single Chameleon parameter.</strong></p>
*
* @return plugin bootstrap class.
*/
@NotNull Class<? extends ChameleonPluginBootstrap> bootstrap() default ChameleonPluginBootstrap.class;

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
*/
package dev.hypera.chameleon.annotations.processing;

import dev.hypera.chameleon.Chameleon;
import dev.hypera.chameleon.ChameleonPluginBootstrap;
import dev.hypera.chameleon.annotations.Plugin;
import dev.hypera.chameleon.annotations.exception.ChameleonAnnotationException;
import dev.hypera.chameleon.annotations.processing.generation.Generator;
Expand All @@ -33,15 +35,23 @@
import dev.hypera.chameleon.annotations.processing.generation.sponge.SpongeGenerator;
import dev.hypera.chameleon.annotations.processing.generation.velocity.VelocityGenerator;
import dev.hypera.chameleon.platform.Platform;
import java.util.Map;
import java.util.Optional;
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.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Chameleon Annotation Processor.
Expand Down Expand Up @@ -74,8 +84,12 @@ public synchronized boolean process(Set<? extends TypeElement> annotations, Roun
}

TypeElement plugin = (TypeElement) element;

Plugin data = plugin.getAnnotation(Plugin.class);

// Plugin bootstrap
TypeElement pluginBootstrap = retrieveBootstrap(plugin);

// Platform "main class" generation
for (String platform : data.platforms()) {
Generator generator;
switch (platform) {
Expand All @@ -98,14 +112,71 @@ public synchronized boolean process(Set<? extends TypeElement> annotations, Roun
generator = new VelocityGenerator();
break;
default:
throw new IllegalStateException("Invalid platform: " + platform);
throw new IllegalStateException("Invalid or unknown platform: " + platform);
}

generator.generate(data, plugin, processingEnv);
generator.generate(data, plugin, pluginBootstrap, processingEnv);
}
}

return false;
}

private @Nullable TypeElement retrieveBootstrap(@NotNull TypeElement plugin) {
TypeMirror bootstrapMirror = getBootstrapFromAnnotation(plugin);
TypeElement bootstrapElement = bootstrapMirror != null ?
(TypeElement) this.processingEnv.getTypeUtils().asElement(bootstrapMirror) : null;
if (bootstrapElement != null && !bootstrapElement.toString().equals(ChameleonPluginBootstrap.class.getName())) {
return bootstrapElement;
}
checkDefaultBootstrapConstructorPresent(plugin);
return null;
}

private void checkDefaultBootstrapConstructorPresent(@NotNull TypeElement plugin) {
// A plugin bootstrap class was not provided, make sure there is a public
// constructor that has a single Chameleon parameter.
Optional<ExecutableElement> constructor = plugin.getEnclosedElements()
.parallelStream()
.filter(e -> e.getKind() == ElementKind.CONSTRUCTOR && e.getModifiers().contains(Modifier.PUBLIC))
.map(e -> (ExecutableElement) e)
.filter(e -> e.getParameters().size() == 1)
.filter(e -> this.processingEnv.getTypeUtils().asElement(e.getParameters().get(0).asType()).toString().equals(Chameleon.class.getName()))
.findAny();
if (constructor.isEmpty()) {
throw new ChameleonAnnotationException(
"If a plugin bootstrap is not provided to @Plugin, the annotated class must have a constructor with a single Chameleon parameter."
);
}
}

/**
* Returns the bootstrap class type mirror from the Plugin annotation.
*
* @param typeElement Type element to read annotation on.
*
* @return bootstrap type mirror, if found, otherwise {@code null}.
*/
private @Nullable TypeMirror getBootstrapFromAnnotation(@NotNull TypeElement typeElement) {
AnnotationMirror mirror = getPluginAnnotationMirror(typeElement);
if (mirror == null) {
return null;
}
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : mirror.getElementValues().entrySet()) {
if (entry.getKey().getSimpleName().toString().equals("bootstrap")) {
return (TypeMirror) entry.getValue().getValue();
}
}
return null;
}

private @Nullable AnnotationMirror getPluginAnnotationMirror(@NotNull TypeElement typeElement) {
for (AnnotationMirror mirror : typeElement.getAnnotationMirrors()) {
if (mirror.getAnnotationType().toString().equals(Plugin.class.getName())) {
return mirror;
}
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
package dev.hypera.chameleon.annotations.processing.generation;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import dev.hypera.chameleon.annotations.Plugin;
Expand All @@ -32,6 +33,7 @@
import javax.lang.model.element.TypeElement;
import org.jetbrains.annotations.ApiStatus.NonExtendable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Platform main class generator.
Expand All @@ -45,13 +47,14 @@ public abstract class Generator {
/**
* Generate main class and any required files.
*
* @param data Plugin data.
* @param plugin Chameleon plugin main class.
* @param env Processing environment.
* @param data Plugin data.
* @param plugin Chameleon plugin main class.
* @param bootstrap Chameleon plugin bootstrap.
* @param env Processing environment.
*
* @throws ChameleonAnnotationException if something goes wrong while generating the files.
*/
public abstract void generate(@NotNull Plugin data, @NotNull TypeElement plugin, @NotNull ProcessingEnvironment env) throws ChameleonAnnotationException;
public abstract void generate(@NotNull Plugin data, @NotNull TypeElement plugin, @Nullable TypeElement bootstrap, @NotNull ProcessingEnvironment env) throws ChameleonAnnotationException;

protected @NotNull ParameterizedTypeName generic(@NotNull ClassName clazz, @NotNull TypeName... arguments) {
return ParameterizedTypeName.get(clazz, arguments);
Expand All @@ -61,4 +64,18 @@ public abstract class Generator {
return ClassName.get(p, n);
}

protected @NotNull MethodSpec.Builder addBootstrap(@NotNull MethodSpec.Builder builder, @NotNull ClassName clazz, @NotNull TypeElement plugin, @Nullable TypeElement bootstrap) {
if (bootstrap == null) {
// Default bootstrap, MyPlugin::new (chameleon -> new MyPlugin(chameleon))
return builder.addStatement(
"this.$N = $T.create($T::new, this).load()",
CHAMELEON_VAR, clazz, plugin
);
}
return builder.addStatement(
"this.$N = $T.create(new $T(), this).load()",
CHAMELEON_VAR, clazz, bootstrap
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,21 @@
import dev.hypera.chameleon.annotations.exception.ChameleonAnnotationException;
import dev.hypera.chameleon.annotations.processing.generation.Generator;
import dev.hypera.chameleon.annotations.utils.MapBuilder;
import dev.hypera.chameleon.exception.instantiation.ChameleonInstantiationException;
import dev.hypera.chameleon.platform.Platform;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.StandardLocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.Yaml;

/**
Expand All @@ -67,16 +66,12 @@ public class BukkitGenerator extends Generator {
* @throws ChameleonAnnotationException if something goes wrong while creating the files.
*/
@Override
public void generate(@NotNull Plugin data, @NotNull TypeElement plugin, @NotNull ProcessingEnvironment env) throws ChameleonAnnotationException {
MethodSpec constructorSpec = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.beginControlFlow("try")
.addStatement("this.$N = $T.create($T.class, this).load()", CHAMELEON_VAR, clazz("dev.hypera.chameleon.platform.bukkit", "BukkitChameleon"), plugin)
.nextControlFlow("catch ($T ex)", ChameleonInstantiationException.class)
.addStatement("getLogger().log($T.SEVERE, \"An error occurred while loading Chameleon\", $N)", Level.class, "ex")
.addStatement("throw new $T($N)", clazz("dev.hypera.chameleon.exception", "ChameleonRuntimeException"), "ex")
.endControlFlow()
.build();
public void generate(@NotNull Plugin data, @NotNull TypeElement plugin, @Nullable TypeElement bootstrap, @NotNull ProcessingEnvironment env) throws ChameleonAnnotationException {
MethodSpec constructorSpec = addBootstrap(
MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC),
clazz("dev.hypera.chameleon.platform.bukkit", "BukkitChameleon"),
plugin, bootstrap
).build();

MethodSpec enableSpec = MethodSpec.methodBuilder("onEnable")
.addAnnotation(Override.class).addModifiers(Modifier.PUBLIC)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,21 @@
import dev.hypera.chameleon.annotations.exception.ChameleonAnnotationException;
import dev.hypera.chameleon.annotations.processing.generation.Generator;
import dev.hypera.chameleon.annotations.utils.MapBuilder;
import dev.hypera.chameleon.exception.instantiation.ChameleonInstantiationException;
import dev.hypera.chameleon.platform.Platform;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.StandardLocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.Yaml;

/**
Expand All @@ -67,17 +66,14 @@ public class BungeeCordGenerator extends Generator {
* @throws ChameleonAnnotationException if something goes wrong while creating the files.
*/
@Override
public void generate(@NotNull Plugin data, @NotNull TypeElement plugin, @NotNull ProcessingEnvironment env) throws ChameleonAnnotationException {
MethodSpec loadSpec = MethodSpec.methodBuilder("onLoad")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.beginControlFlow("try")
.addStatement("this.$N = $T.create($T.class, this).load()", CHAMELEON_VAR, clazz("dev.hypera.chameleon.platform.bungeecord", "BungeeCordChameleon"), plugin)
.nextControlFlow("catch ($T ex)", ChameleonInstantiationException.class)
.addStatement("getLogger().log($T.SEVERE, \"An error occurred while loading Chameleon\", $N)", Level.class, "ex")
.addStatement("throw new $T($N)", clazz("dev.hypera.chameleon.exception", "ChameleonRuntimeException"), "ex")
.endControlFlow()
.build();
public void generate(@NotNull Plugin data, @NotNull TypeElement plugin, @Nullable TypeElement bootstrap, @NotNull ProcessingEnvironment env) throws ChameleonAnnotationException {
MethodSpec loadSpec = addBootstrap(
MethodSpec.methodBuilder("onLoad")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC),
clazz("dev.hypera.chameleon.platform.bungeecord", "BungeeCordChameleon"),
plugin, bootstrap
).build();

MethodSpec enableSpec = MethodSpec.methodBuilder("onEnable")
.addAnnotation(Override.class).addModifiers(Modifier.PUBLIC)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,21 @@
import dev.hypera.chameleon.annotations.exception.ChameleonAnnotationException;
import dev.hypera.chameleon.annotations.processing.generation.Generator;
import dev.hypera.chameleon.annotations.utils.MapBuilder;
import dev.hypera.chameleon.exception.instantiation.ChameleonInstantiationException;
import dev.hypera.chameleon.platform.Platform;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.StandardLocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.Yaml;

/**
Expand All @@ -66,16 +65,13 @@ public final class FoliaGenerator extends Generator {
* @throws ChameleonAnnotationException if something goes wrong while creating the files.
*/
@Override
public void generate(@NotNull Plugin data, @NotNull TypeElement plugin, @NotNull ProcessingEnvironment env) throws ChameleonAnnotationException {
MethodSpec constructorSpec = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.beginControlFlow("try")
.addStatement("this.$N = $T.create($T.class, this).load()", CHAMELEON_VAR, clazz("dev.hypera.chameleon.platform.folia", "FoliaChameleon"), plugin)
.nextControlFlow("catch ($T ex)", ChameleonInstantiationException.class)
.addStatement("getLogger().log($T.SEVERE, \"An error occurred while loading Chameleon\", $N)", Level.class, "ex")
.addStatement("throw new $T($N)", clazz("dev.hypera.chameleon.exception", "ChameleonRuntimeException"), "ex")
.endControlFlow()
.build();
public void generate(@NotNull Plugin data, @NotNull TypeElement plugin, @Nullable TypeElement bootstrap, @NotNull ProcessingEnvironment env) throws ChameleonAnnotationException {
MethodSpec constructorSpec = addBootstrap(
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC),
clazz("dev.hypera.chameleon.platform.folia", "FoliaChameleon"),
plugin, bootstrap
).build();

MethodSpec enableSpec = MethodSpec.methodBuilder("onEnable")
.addAnnotation(Override.class).addModifiers(Modifier.PUBLIC)
Expand Down
Loading

0 comments on commit f6e5b38

Please sign in to comment.