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 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 : examples.http.Server maps to http/server.js, http/server.groovy, etc...

The processor is only active when the option 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 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 package element is mandatory. Langs is optional. When not set, all languages are skipped. Languages are identified by their extensions.

Author:Julien Viet, Clement Escoffier
/** * 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.<p/> * <p> * 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...<p/> * <p> * 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 <i>codetrans.log</i> will also be written with the processor activity. * <p> * 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: * <p> * <code><pre> * { * "excludes": [ * { * "package" : "the (java) package to exclude", * "langs" : ["lang1", "lang2"] * } * ] * } * </pre></code> * <p> * The {@code package} element is mandatory. {@code Langs} is optional. When not set, all languages are skipped. * Languages are identified by their <em>extensions</em>. * * @author <a href="mailto:julien@julienviet.com">Julien Viet</a> * @author <a href="mailto:clement@apache.org">Clement Escoffier</a> */
public class CodeTransProcessor extends AbstractProcessor { private File outputDir; private CodeTranslator translator; private List<Lang> langs; private Set<File> folders = new HashSet<>(); // The copied folders so we don't do the job twice private PrintWriter log; private ObjectNode config; private Map<String, Set<String>> abc = new HashMap<>(); private RenderMode renderMode; @Override public Set<String> getSupportedOptions() { return Collections.singleton("codetrans.output"); } @Override public Set<String> 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<String> 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<String> 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<Path> copyingVisitor = new SimpleFileVisitor<Path>() { @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<ExecutableElement, Boolean> 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<Modifier> 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<ExecutableElement, Boolean> 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<String> 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<String> 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.
Params:
  • type – the type
  • lang – the language
Returns:true if the translation is skipped, false otherwise.
/** * 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; } }