xyz.block.ftl.runtime.processor.AnnotationProcessor Maven / Gradle / Ivy
package xyz.block.ftl.runtime.processor;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.processing.Completion;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import xyz.block.ftl.Config;
import xyz.block.ftl.Enum;
import xyz.block.ftl.Export;
import xyz.block.ftl.Secret;
import xyz.block.ftl.TypeAlias;
import xyz.block.ftl.Verb;
/**
* POC annotation processor for capturing JavaDoc, this needs a lot more work.
*/
public class AnnotationProcessor implements Processor {
private static final Pattern REMOVE_LEADING_SPACE = Pattern.compile("^ ", Pattern.MULTILINE);
private static final Pattern REMOVE_JAVADOC_TAGS = Pattern.compile(
"^\\s*@(param|return|throws|exception|see|author)\\b[^\\n]*$\\n*",
Pattern.MULTILINE);
private ProcessingEnvironment processingEnv;
final Map saved = new HashMap<>();
@Override
public Set getSupportedOptions() {
return Set.of();
}
@Override
public Set getSupportedAnnotationTypes() {
return Set.of(Verb.class.getName(), Enum.class.getName(), Export.class.getName(), TypeAlias.class.getName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public void init(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
//TODO: @VerbName, HTTP, CRON etc
roundEnv.getElementsAnnotatedWithAny(Set.of(Verb.class, Enum.class, Export.class, TypeAlias.class))
.forEach(element -> {
Optional javadoc = getJavadoc(element);
javadoc.ifPresent(doc -> {
String strippedDownDoc = stripJavadocTags(doc);
if (element.getAnnotation(TypeAlias.class) != null) {
saved.put(element.getAnnotation(TypeAlias.class).name(), strippedDownDoc);
} else {
saved.put(element.getSimpleName().toString(), strippedDownDoc);
}
if (element.getKind() == ElementKind.METHOD) {
var executableElement = (ExecutableElement) element;
executableElement.getParameters().forEach(param -> {
Config config = param.getAnnotation(Config.class);
if (config != null) {
saved.put(config.value(), extractCommentForParam(doc, param));
}
Secret secret = param.getAnnotation(Secret.class);
if (secret != null) {
saved.put(secret.value(), extractCommentForParam(doc, param));
}
});
}
});
});
if (roundEnv.processingOver()) {
write("META-INF/ftl-verbs.txt", saved.entrySet().stream().map(
e -> e.getKey() + "=" + Base64.getEncoder().encodeToString(e.getValue().getBytes(StandardCharsets.UTF_8)))
.collect(Collectors.toSet()));
}
return false;
}
/**
* This method uses the annotation processor Filer API and we shouldn't use a Path as paths containing \ are not supported.
*/
public void write(String filePath, Set set) {
if (set.isEmpty()) {
return;
}
try {
final FileObject listResource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "",
filePath.toString());
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(listResource.openOutputStream(), StandardCharsets.UTF_8))) {
for (String className : set) {
writer.write(className);
writer.newLine();
}
}
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write " + filePath + ": " + e);
return;
}
}
@Override
public Iterable extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member,
String userText) {
return null;
}
public Optional getJavadoc(Element e) {
String docComment = processingEnv.getElementUtils().getDocComment(e);
if (docComment == null || docComment.isBlank()) {
return Optional.empty();
}
// javax.lang.model keeps the leading space after the "*" so we need to remove it.
return Optional.of(REMOVE_LEADING_SPACE.matcher(docComment)
.replaceAll("")
.trim());
}
public String stripJavadocTags(String doc) {
// TODO extract JavaDoc tags to a rich markdown model supported by schema
return REMOVE_JAVADOC_TAGS.matcher(doc).replaceAll("");
}
/**
* Read the @param tag in a JavaDoc comment to extract Config and Secret comments
*/
private String extractCommentForParam(String doc, VariableElement param) {
String variableName = param.getSimpleName().toString();
int startIdx = doc.indexOf("@param " + variableName + " ");
if (startIdx != -1) {
int endIndex = doc.indexOf("\n", startIdx);
if (endIndex == -1) {
endIndex = doc.length();
}
return doc.substring(startIdx + variableName.length() + 8, endIndex);
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy