Skip to content

Commit

Permalink
✨ Velocity support
Browse files Browse the repository at this point in the history
  • Loading branch information
itsTyrion committed Feb 9, 2024
1 parent e6c6ca4 commit b588ed5
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 87 deletions.
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ repositories {
}

group 'de.itsTyrion'
version '1.2'
version '1.3'

dependencies {
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
compileOnly 'org.projectlombok:lombok:1.18.30'
compileOnly 'org.jetbrains:annotations:24.1.0'
implementation 'com.grack:nanojson:1.7'
}

compileJava {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
package de.itsTyrion.pluginAnnotation;

import de.itsTyrion.pluginAnnotation.util.Generator;
import de.itsTyrion.pluginAnnotation.velocity.VelocityPlugin;
import lombok.val;

import javax.annotation.processing.*;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
import javax.tools.StandardLocation;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Set;
import java.util.function.BiConsumer;

@SuppressWarnings("Since15") // This is only about the SupportedSourceVersion annotation
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedOptions(value = {"mcPluginVersion", "spigotLibraries"})
@SupportedAnnotationTypes({"de.itsTyrion.pluginAnnotation.Plugin", "de.itsTyrion.pluginAnnotation.BungeePlugin"})
@SupportedAnnotationTypes({
"de.itsTyrion.pluginAnnotation.Plugin",
"de.itsTyrion.pluginAnnotation.BungeePlugin",
"de.itsTyrion.pluginAnnotation.velocity.VelocityPlugin"})
public class PluginAnnotationProcessor extends AbstractProcessor {

private String pluginMainClassFound = null;
private String bungeePluginMainClassFound = null;
private String velocityPluginMainClassFound = null;

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

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Expand All @@ -32,8 +41,6 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
}

for (val element : roundEnv.getElementsAnnotatedWith(Plugin.class)) {
val pluginAnnotation = element.getAnnotation(Plugin.class);

// fully qualified name, required for plugin.yml `main` property
val fqName = ((TypeElement) element).getQualifiedName().toString();

Expand All @@ -51,12 +58,12 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
val commandInfos = roundEnv.getElementsAnnotatedWith(CommandInfo.class).stream()
.map(element1 -> element1.getAnnotation(CommandInfo.class)).toArray(CommandInfo[]::new);

val content = generatePluginYmlContent(pluginAnnotation, fqName, projectVersion, libraries, commandInfos);
writeYml("plugin.yml", content, fqName);
val pluginAnnotation = element.getAnnotation(Plugin.class);
val content = Generator.pluginYML(pluginAnnotation, fqName, projectVersion, libraries, commandInfos);
writeResource("plugin.yml", content, fqName);
}
for (val element : roundEnv.getElementsAnnotatedWith(BungeePlugin.class)) {
val pluginAnnotation = element.getAnnotation(BungeePlugin.class);

for (val element : roundEnv.getElementsAnnotatedWith(BungeePlugin.class)) {
// fully qualified name, required for bungee.yml `main` property
val fqName = ((TypeElement) element).getQualifiedName().toString();

Expand All @@ -67,14 +74,37 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
}
bungeePluginMainClassFound = fqName;

val content = generateBungeeYmlContent(pluginAnnotation, fqName, projectVersion);
writeYml("bungee.yml", content, fqName);
val pluginAnnotation = element.getAnnotation(BungeePlugin.class);
val content = Generator.bungeeYML(pluginAnnotation, fqName, projectVersion);
writeResource("bungee.yml", content, fqName);
}

for (val element : roundEnv.getElementsAnnotatedWith(VelocityPlugin.class)) {
// fully qualified name, required for velocity-plugin.json `main` property
val fqName = ((TypeElement) element).getQualifiedName().toString();

if (velocityPluginMainClassFound != null && !velocityPluginMainClassFound.equals(fqName)) {
processingEnv.getMessager()
.printMessage(Kind.ERROR, "Multiple plugin main classes are unsupported! Using `" + fqName + "`.");
return false;
}
velocityPluginMainClassFound = fqName;

val plugin = element.getAnnotation(VelocityPlugin.class);
if (!Generator.VELOCITY_ID_PATTERN.matcher(plugin.id()).matches()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Invalid ID for plugin " + fqName + ". IDs must start alphabetically," +
"have lowercase alphanumeric characters, and can contain dashes or underscores.");
return false;
}
val content = Generator.velocityPluginJSON(plugin, fqName, projectVersion);
writeResource("velocity-plugin.json", content, fqName);
}

return true;
}

private void writeYml(String name, String content, String fqName) {
private void writeResource(String name, String content, String fqName) {
processingEnv.getMessager()
.printMessage(Kind.NOTE, "Processed plugin annotation on `" + fqName + '`');

Expand All @@ -86,75 +116,4 @@ private void writeYml(String name, String content, String fqName) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Error while writing " + name + ':' + e);
}
}

private String generatePluginYmlContent(Plugin plugin, String fqName, String version, String[] libraries,
CommandInfo[] commands) {
val builder = new StringBuilder()
.append("name: ").append(plugin.name()).append('\n')
.append("version: ").append(plugin.version().replace("%mcPluginVersion%", version)).append('\n')
.append("main: ").append(fqName).append('\n')
.append("api-version: ").append(plugin.apiVersion()).append('\n')
.append("load: ").append(plugin.load().name()).append('\n');

appendIfPresent(builder, "depend", plugin.depend());
appendIfPresent(builder, "authors", plugin.authors());
appendIfPresent(builder, "contributors", plugin.contributors());
appendIfPresent(builder, "loadbefore", plugin.loadBefore());
appendIfPresent(builder, "provides", plugin.provides());
appendIfPresent(builder, "softdepend", plugin.softDepend());
appendIfPresent(builder, "libraries", libraries);

appendIfPresent(builder, "website", plugin.website());
if (notBlank(plugin.description()))
appendIfPresent(builder, "description", '"' + plugin.description().replace("\n", "\\n") + '"');
appendIfPresent(builder, "prefix", plugin.logPrefix());

builder.append('\n');

if (commands.length != 0) {
val sb = new StringBuilder("commands:\n");
for (CommandInfo ci : commands) {
sb.append(" ").append(ci.name()).append(": ").append('\n');
sb.append(" aliases: ").append(Arrays.toString(ci.aliases())).append('\n');

BiConsumer<String, String> append = (k, v) -> {if (notBlank(v)) sb.append(k).append(v).append('\n');};

append.accept(" description: ", ci.description());
append.accept(" usage: ", ci.usage());
append.accept(" permission: ", ci.permission());
append.accept(" permission-message: ", ci.permissionMessage());
}
builder.append(sb);
}

return builder.toString();
}


private String generateBungeeYmlContent(BungeePlugin plugin, String fqName, String version) {
val builder = new StringBuilder()
.append("name: ").append(plugin.name()).append('\n')
.append("version: ").append(plugin.version().replace("%mcPluginVersion%", version)).append('\n')
.append("main: ").append(fqName).append('\n');

appendIfPresent(builder, "depends", plugin.depends());
appendIfPresent(builder, "softdepends", plugin.softDepends());

appendIfPresent(builder, "author", plugin.author());
appendIfPresent(builder, "description", plugin.description());

return builder.toString();
}

private void appendIfPresent(StringBuilder builder, String key, String[] value) {
if (value.length > 0) // The format of Arrays.toString is a valid YAML list - how convenient.
builder.append(key).append(": ").append(Arrays.toString(value)).append('\n');
}

private void appendIfPresent(StringBuilder builder, String key, String value) {
if (notBlank(value))
builder.append(key).append(": ").append(value).append('\n');
}

private boolean notBlank(String str) {return !str.isEmpty() && !str.chars().allMatch(Character::isWhitespace);}
}
118 changes: 118 additions & 0 deletions src/main/java/de/itsTyrion/pluginAnnotation/util/Generator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package de.itsTyrion.pluginAnnotation.util;

import com.grack.nanojson.JsonWriter;
import de.itsTyrion.pluginAnnotation.BungeePlugin;
import de.itsTyrion.pluginAnnotation.CommandInfo;
import de.itsTyrion.pluginAnnotation.Plugin;
import de.itsTyrion.pluginAnnotation.velocity.VelocityPlugin;
import lombok.val;

import java.util.Arrays;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public final class Generator {
private Generator() {}


public static String pluginYML(Plugin plugin, String fqName, String version, String[] libraries,
CommandInfo[] commands) {
val builder = new StringBuilder()
.append("name: ").append(plugin.name()).append('\n')
.append("version: ").append(plugin.version().replace("%mcPluginVersion%", version)).append('\n')
.append("main: ").append(fqName).append('\n')
.append("api-version: ").append(plugin.apiVersion()).append('\n')
.append("load: ").append(plugin.load().name()).append('\n');

appendIfPresent(builder, "depend", plugin.depend());
appendIfPresent(builder, "authors", plugin.authors());
appendIfPresent(builder, "contributors", plugin.contributors());
appendIfPresent(builder, "loadbefore", plugin.loadBefore());
appendIfPresent(builder, "provides", plugin.provides());
appendIfPresent(builder, "softdepend", plugin.softDepend());
appendIfPresent(builder, "libraries", libraries);

appendIfPresent(builder, "website", plugin.website());
if (notBlank(plugin.description()))
appendIfPresent(builder, "description", '"' + plugin.description().replace("\n", "\\n") + '"');
appendIfPresent(builder, "prefix", plugin.logPrefix());

builder.append('\n');

if (commands.length != 0) {
val sb = new StringBuilder("commands:\n");
for (CommandInfo ci : commands) {
sb.append(" ").append(ci.name()).append(": ").append('\n');
sb.append(" aliases: ").append(Arrays.toString(ci.aliases())).append('\n');

BiConsumer<String, String> append = (k, v) -> {if (notBlank(v)) sb.append(k).append(v).append('\n');};

append.accept(" description: ", ci.description());
append.accept(" usage: ", ci.usage());
append.accept(" permission: ", ci.permission());
append.accept(" permission-message: ", ci.permissionMessage());
}
builder.append(sb);
}

return builder.toString();
}


public static String bungeeYML(BungeePlugin plugin, String fqName, String version) {
val builder = new StringBuilder()
.append("name: ").append(plugin.name()).append('\n')
.append("version: ").append(plugin.version().replace("%mcPluginVersion%", version)).append('\n')
.append("main: ").append(fqName).append('\n');

appendIfPresent(builder, "depends", plugin.depends());
appendIfPresent(builder, "softdepends", plugin.softDepends());

appendIfPresent(builder, "author", plugin.author());
appendIfPresent(builder, "description", plugin.description());

return builder.toString();
}


public static final Pattern VELOCITY_ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}");

public static String velocityPluginJSON(VelocityPlugin vp, String qualifiedName, String version) {
for (val dependency : vp.dependencies())
if (!VELOCITY_ID_PATTERN.matcher(dependency.id()).matches())
throw new IllegalArgumentException("id of dependency " + dependency.id() + " is not valid");
val json = JsonWriter.string()
.object()
.value("id", vp.id());

if (!vp.name().isEmpty()) json.value("name", vp.name());
json.value("version", version);
if (!vp.description().isEmpty()) json.value("description", vp.description());
if (!vp.url().isEmpty()) json.value("url", vp.url());
val authors = Arrays.stream(vp.authors()).filter(author -> !author.isEmpty()).collect(Collectors.toList());
if (!authors.isEmpty()) json.array("authors", authors);
if (vp.dependencies().length > 0) {
json.array("dependencies");
for (val dependency : vp.dependencies())
json.object().value("id", dependency.id()).value("optional", dependency.optional()).end();
json.end();
}
json.value("main", qualifiedName);
return json.end().done();
}

private static void appendIfPresent(StringBuilder builder, String key, String[] value) {
if (value.length > 0) // The format of Arrays.toString is a valid YAML list - how convenient.
builder.append(key).append(": ").append(Arrays.toString(value)).append('\n');
}

private static void appendIfPresent(StringBuilder builder, String key, String value) {
if (notBlank(value))
builder.append(key).append(": ").append(value).append('\n');
}

private static boolean notBlank(String str) {
return !str.isEmpty() && !str.chars().allMatch(Character::isWhitespace);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2018-2021 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/

package de.itsTyrion.pluginAnnotation.velocity;

import java.lang.annotation.Target;

@Target({})
public @interface Dependency {

/**
* The plugin ID of the dependency.
* @see VelocityPlugin#id()
*/
String id();

/**
* Whether the dependency is not required to enable this plugin. By default, this is
* {@code false}, meaning that the dependency is required to enable this plugin.
*/
boolean optional() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2018-2021 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/

package de.itsTyrion.pluginAnnotation.velocity;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
public @interface VelocityPlugin {

/**
* The plugin's ID. This ID should be unique as to not conflict with other plugins. The plugin ID
* may contain alphanumeric characters, dashes, and underscores, and be a maximum of 64 characters long.
*/
String id();

/**
* The human-readable name of the plugin as to be used in descriptions and similar things.
*/
String name() default "";

/**
* The plugin description, briefly explaining its use.
*/
String description() default "";

/**
* The plugin's website/URL, or an empty string if unknown
*/
String url() default "";

/**
* The plugin's author, or empty if unknown
*/
String[] authors() default {};

/**
* The dependencies required to load before this plugin.
*/
Dependency[] dependencies() default {};
}

0 comments on commit b588ed5

Please sign in to comment.