From cafa316a289ba88927bf3428ebb045ee445bcb37 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 23 Aug 2024 10:06:46 +1000 Subject: [PATCH] feat: use JavaDoc as comments (#2481) This is a very basic PoC implementation that can serve as a base for further work. --- .../block/ftl/deployment/ModuleBuilder.java | 8 +- .../block/ftl/deployment/ModuleProcessor.java | 20 ++- .../runtime/it/FtlJavaRuntimeResource.java | 7 + .../processor/AnnotationProcessor.java | 120 ++++++++++++++++++ .../javax.annotation.processing.Processor | 1 + 5 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 jvm-runtime/ftl-runtime/java/runtime/src/main/java/xyz/block/ftl/runtime/processor/AnnotationProcessor.java create mode 100644 jvm-runtime/ftl-runtime/java/runtime/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index 0af0647e3..cae1ba5f5 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -82,9 +82,11 @@ public class ModuleBuilder { private final Map knownTopics; private final Map verbClients; private final FTLRecorder recorder; + private final Map verbDocs; public ModuleBuilder(IndexView index, String moduleName, Map knownTopics, - Map verbClients, FTLRecorder recorder) { + Map verbClients, FTLRecorder recorder, + Map verbDocs) { this.index = index; this.moduleName = moduleName; this.moduleBuilder = Module.newBuilder() @@ -93,6 +95,7 @@ public ModuleBuilder(IndexView index, String moduleName, Map schemaContributorBuildItems) throws Exception { String moduleName = moduleNameBuildItem.getModuleName(); + Map verbDocs = new HashMap<>(); + try (var input = Thread.currentThread().getContextClassLoader().getResourceAsStream("META-INF/ftl-verbs.txt")) { + if (input != null) { + var contents = new String(input.readAllBytes(), StandardCharsets.UTF_8).split("\n"); + for (var content : contents) { + var eq = content.indexOf('='); + if (eq == -1) { + continue; + } + String key = content.substring(0, eq); + String value = new String(Base64.getDecoder().decode(content.substring(eq + 1)), StandardCharsets.UTF_8); + verbDocs.put(key, value); + } + } + } ModuleBuilder moduleBuilder = new ModuleBuilder(index.getComputingIndex(), moduleName, topicsBuildItem.getTopics(), - verbClientBuildItem.getVerbClients(), recorder); + verbClientBuildItem.getVerbClients(), recorder, verbDocs); for (var i : schemaContributorBuildItems) { i.getSchemaContributor().accept(moduleBuilder); diff --git a/jvm-runtime/ftl-runtime/java/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResource.java b/jvm-runtime/ftl-runtime/java/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResource.java index 85907b006..b76cc737a 100644 --- a/jvm-runtime/ftl-runtime/java/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResource.java +++ b/jvm-runtime/ftl-runtime/java/integration-tests/src/main/java/xyz/block/ftl/java/runtime/it/FtlJavaRuntimeResource.java @@ -46,6 +46,13 @@ public String bytesHttp(byte[] data) { return "Hello " + new String(data, StandardCharsets.UTF_8); } + /** + * This is a verb FOOO + * + * @param name + * @param echoClient + * @return + */ @Verb public String hello(String name, EchoClient echoClient) { return "Hello " + echoClient.call(new EchoRequest().setName(name)).getMessage(); diff --git a/jvm-runtime/ftl-runtime/java/runtime/src/main/java/xyz/block/ftl/runtime/processor/AnnotationProcessor.java b/jvm-runtime/ftl-runtime/java/runtime/src/main/java/xyz/block/ftl/runtime/processor/AnnotationProcessor.java new file mode 100644 index 000000000..99c92eff6 --- /dev/null +++ b/jvm-runtime/ftl-runtime/java/runtime/src/main/java/xyz/block/ftl/runtime/processor/AnnotationProcessor.java @@ -0,0 +1,120 @@ +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.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +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 ProcessingEnvironment processingEnv; + + final Map saved = new HashMap<>(); + + @Override + public Set getSupportedOptions() { + return Set.of(); + } + + @Override + public Set getSupportedAnnotationTypes() { + return Set.of(Verb.class.getName()); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public void init(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + //TODO: @VerbName, HTTP, CRON etc + roundEnv.getElementsAnnotatedWith(Verb.class) + .forEach(element -> { + Optional javadoc = getJavadoc(element); + javadoc.ifPresent(doc -> saved.put(element.getSimpleName().toString(), doc)); + }); + + 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 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()); + } + +} diff --git a/jvm-runtime/ftl-runtime/java/runtime/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/jvm-runtime/ftl-runtime/java/runtime/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000..c09a4e9fb --- /dev/null +++ b/jvm-runtime/ftl-runtime/java/runtime/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +xyz.block.ftl.runtime.processor.AnnotationProcessor