io.vertx.codetrans.CodeTransProcessor Maven / Gradle / Ivy
package io.vertx.codetrans;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import io.vertx.codetrans.annotations.CodeTranslate;
import io.vertx.codetrans.lang.groovy.GroovyLang;
import io.vertx.codetrans.lang.js.JavaScriptLang;
import io.vertx.codetrans.lang.kotlin.KotlinLang;
import io.vertx.codetrans.lang.ruby.RubyLang;
import io.vertx.codetrans.lang.scala.ScalaLang;
import io.vertx.core.Verticle;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
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 javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A processor plugin generate scripts from {@link io.vertx.core.Verticle} class. It scans all the compiled
* classes and tries to generate corresponding scripts for each class.
*
* The script is named after the verticle fqn using the last atom of the package name and the lower
* cased class name, for example : {@code examples.http.Server} maps to {@code http/server.js},
* {@code http/server.groovy}, etc...
*
* The processor is only active when the option {@code codetrans.output} is set to a valid directory where the scripts
* will be written. A log codetrans.log will also be written with the processor activity.
*
* The processor can be configured using the {@code condetrans.config} property targeting a JSON file. The JSON file
* contains a set of exclusions and is structured as follows:
*
*
* {
* "excludes": [
* {
* "package" : "the (java) package to exclude",
* "langs" : ["lang1", "lang2"]
* }
* ]
* }
*
*
* The {@code package} element is mandatory. {@code Langs} is optional. When not set, all languages are skipped.
* Languages are identified by their extensions.
*
* @author Julien Viet
* @author Clement Escoffier
*/
public class CodeTransProcessor extends AbstractProcessor {
private File outputDir;
private CodeTranslator translator;
private List langs;
private Set folders = new HashSet<>(); // The copied folders so we don't do the job twice
private PrintWriter log;
private ObjectNode config;
private Map> abc = new HashMap<>();
private RenderMode renderMode;
@Override
public Set getSupportedOptions() {
return Collections.singleton("codetrans.output");
}
@Override
public Set getSupportedAnnotationTypes() {
return Collections.singleton("*");
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
String outputOption = processingEnv.getOptions().get("codetrans.output");
if (outputOption != null) {
outputDir = new File(outputOption);
}
translator = new CodeTranslator(processingEnv);
langs = new ArrayList<>();
String renderOpt = processingEnv.getOptions().get("codetrans.render");
renderMode = renderOpt != null ? RenderMode.valueOf(renderOpt.toUpperCase()) : RenderMode.EXAMPLE;
String langsOpt = processingEnv.getOptions().get("codetrans.langs");
Set langs;
if (langsOpt != null) {
langs = new HashSet<>(Arrays.asList(langsOpt.split("\\s*,\\s*")));
} else {
langs = new HashSet<>(Arrays.asList("js", "ruby", "kotlin", "groovy"));
}
String configOpt = processingEnv.getOptions().get("codetrans.config");
if (configOpt != null) {
ObjectMapper mapper = new ObjectMapper()
.enable(JsonParser.Feature.ALLOW_COMMENTS)
.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
File file = new File(configOpt);
try {
config = (ObjectNode) mapper.readTree(file);
} catch (IOException e) {
System.err.println("[ERROR] Cannot read configuration file " + file.getAbsolutePath() + " : " + e.getMessage());
e.printStackTrace();
}
}
for (String lang : langs) {
Lang l;
switch (lang) {
case "kotlin":
l = new KotlinLang();
break;
case "groovy":
l = new GroovyLang();
break;
case "js":
l = new JavaScriptLang();
break;
case "scala":
l = new ScalaLang();
break;
case "ruby":
l = new RubyLang();
break;
default:
continue;
}
this.langs.add(l);
if (config != null) {
JsonNode n = config.get(lang);
if (n != null && n.getNodeType() == JsonNodeType.OBJECT) {
JsonNode excludes = n.get("excludes");
if (excludes != null && excludes.getNodeType() == JsonNodeType.ARRAY) {
Set t = new HashSet<>();
abc.put(l.id(), t);
for (int i = 0;i < excludes.size();i++) {
JsonNode c = excludes.get(i);
if (c.getNodeType() == JsonNodeType.STRING) {
TextNode tn = (TextNode) c;
t.add(tn.asText());
}
}
}
}
}
}
}
private PrintWriter getLogger() throws Exception {
if (log == null) {
log = new PrintWriter(new FileWriter(new File(outputDir, "codetrans.log"), false), true);
}
return log;
}
private void copyDirRec(File srcFolder, File dstFolder, PrintWriter log) throws Exception {
if (!folders.contains(dstFolder)) {
folders.add(dstFolder);
Path srcPath = srcFolder.toPath();
Path dstPath = dstFolder.toPath();
SimpleFileVisitor copyingVisitor = new SimpleFileVisitor() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetPath = dstPath.resolve(srcPath.relativize(dir));
if (!Files.exists(targetPath)) {
log.println("Creating dir " + targetPath);
Files.createDirectory(targetPath);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path srcFile, BasicFileAttributes attrs) throws IOException {
if (!srcFile.getFileName().toString().endsWith(".java")) {
log.println("Copying resource " + srcFile + " to " + dstPath);
Path dstFile = dstPath.resolve(srcPath.relativize(srcFile));
Files.copy(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING);
}
return FileVisitResult.CONTINUE;
}
};
Files.walkFileTree(srcPath, copyingVisitor);
}
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
if (log != null) {
log.close();
}
return false;
}
if (outputDir != null && (outputDir.exists() || outputDir.mkdirs())) {
LinkedHashMap methods = new LinkedHashMap<>();
try {
PrintWriter log = getLogger();
// Process all verticles automatically
TypeMirror verticleType = processingEnv.getElementUtils().getTypeElement(Verticle.class.getName()).asType();
for (Element rootElt : roundEnv.getRootElements()) {
Set modifiers = rootElt.getModifiers();
if (rootElt.getKind() == ElementKind.CLASS &&
!modifiers.contains(Modifier.ABSTRACT) &&
modifiers.contains(Modifier.PUBLIC) &&
processingEnv.getTypeUtils().isSubtype(rootElt.asType(), verticleType)) {
TypeElement typeElt = (TypeElement) rootElt;
for (Element enclosedElt : typeElt.getEnclosedElements()) {
if (enclosedElt.getKind() == ElementKind.METHOD) {
ExecutableElement methodElt = (ExecutableElement) enclosedElt;
if (methodElt.getSimpleName().toString().equals("start") && methodElt.getParameters().isEmpty()) {
methods.put(methodElt, true);
}
}
}
}
}
// Process CodeTranslate annotations
roundEnv.getElementsAnnotatedWith(CodeTranslate.class).forEach(annotatedElt -> {
methods.put((ExecutableElement) annotatedElt, false);
});
// Generate
for (Map.Entry method : methods.entrySet()) {
ExecutableElement methodElt = method.getKey();
boolean isVerticle = method.getValue();
TypeElement typeElt = (TypeElement) methodElt.getEnclosingElement();
FileObject obj = processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH, "", typeElt.getQualifiedName().toString().replace('.', '/') + ".java");
File srcFolder = new File(obj.toUri()).getParentFile();
for (Lang lang : langs) {
if (isSkipped(typeElt, lang) || isSkipped(methodElt, lang)) {
log.write("Skipping " + lang.id() + " translation for " + typeElt.getQualifiedName() + "#" +
methodElt.getSimpleName());
continue;
}
List fqn = Arrays.asList(typeElt.toString().split("\\."));
File dstFolder = new File(outputDir, lang.id());
File f = lang.createSourceFile(dstFolder, fqn, !isVerticle ? methodElt.getSimpleName().toString() : null);
if (f.getParentFile().exists() || f.getParentFile().mkdirs()) {
try {
String translation = translator.translate(methodElt, isVerticle, lang, renderMode);
Files.write(f.toPath(), translation.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
log.println("Generated " + f.getAbsolutePath());
copyDirRec(srcFolder, f.getParentFile(), log);
} catch (Exception e) {
log.println("Skipping generation of " + typeElt.getQualifiedName());
e.printStackTrace(log);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
} else {
return false;
}
}
private boolean isSkipped(ExecutableElement methodElt, Lang lang) {
Set excl = abc.get(lang.id());
if (excl != null) {
TypeElement typeElt = (TypeElement) methodElt.getEnclosingElement();
String match = "" + typeElt.getQualifiedName();
if (excl.contains(match)) {
return true;
}
match += "#" + methodElt.getSimpleName();
if (excl.contains(match)) {
return true;
}
}
return false;
}
/**
* Checks whether the generation of the given class to the given lang is explicitly excluded. Exclusions are
* managed in the configuration file. If no configuration file is provided, the translation is not skipped.
*
* @param type the type
* @param lang the language
* @return {@code true} if the translation is skipped, {@code false} otherwise.
*/
private boolean isSkipped(TypeElement type, Lang lang) {
if (config == null) {
// no config, no exclusions
return false;
}
ArrayNode excludes = (ArrayNode) config.get("excludes");
for (JsonNode exclude : excludes) {
// Structure:
// {
// "package": "the package to exclude", (mandatory)
// "langs": ["lang 1", "lang 2"]
// }
// If not langs - skip all languages
if (exclude.get("package") == null) {
throw new IllegalStateException("Malformed configuration - Missing 'package' attribute in the 'codetrans" +
".config' file");
}
String pck = exclude.get("package").asText();
ArrayNode langs = (ArrayNode) exclude.get("langs");
if (type.getQualifiedName().toString().startsWith(pck) && isLanguageSkipped(langs, lang)) {
return true;
}
}
return false;
}
private boolean isLanguageSkipped(ArrayNode langs, Lang lang) {
if (langs == null) {
// If not langs, exclude all.
return true;
}
for (JsonNode node : langs) {
if (node.asText().equalsIgnoreCase(lang.getExtension())) {
return true;
}
}
return false;
}
}