package io.vertx.example.util; 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.ObjectNode; import io.vertx.codegen.Case; import io.vertx.codetrans.CodeTranslator; import io.vertx.codetrans.Lang; 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.ruby.RubyLang; 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.*; 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.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; /** * 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; @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 = Arrays.asList(new JavaScriptLang(), new GroovyLang(), new RubyLang()); String configFile = processingEnv.getOptions().get("codetrans.config"); if (configFile != null) { ObjectMapper mapper = new ObjectMapper() .enable(JsonParser.Feature.ALLOW_COMMENTS) .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); File file = new File(configFile); try { config = (ObjectNode) mapper.readTree(file); } catch (IOException e) { System.err.println("[ERROR] Cannot read configuration file " + file.getAbsolutePath() + " : " + e.getMessage()); e.printStackTrace(); } } } 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())) { List<ExecutableElement> methodElts = new ArrayList<>(); 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()) { methodElts.add(methodElt); } } } } } // Process CodeTranslate annotations roundEnv.getElementsAnnotatedWith(CodeTranslate.class).forEach(annotatedElt -> { methodElts.add((ExecutableElement) annotatedElt); }); // Generate for (ExecutableElement methodElt : methodElts) { 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(); String filename = Case.SNAKE.format(Case.CAMEL.parse(typeElt.getSimpleName().toString())); for (Lang lang : langs) { if (isSkipped(typeElt, lang)) { log.write("Skipping " + lang.getExtension() + " translation for " + typeElt.getQualifiedName() + "#" + methodElt.getSimpleName()); continue; } String folderPath = processingEnv.getElementUtils().getPackageOf(typeElt).getQualifiedName().toString().replace('.', '/'); File dstFolder = new File(new File(outputDir, lang.getExtension()), folderPath); if (dstFolder.exists() || dstFolder.mkdirs()) { try { String translation = translator.translate(methodElt, lang); File f = new File(dstFolder, filename + "." + lang.getExtension()); Files.write(f.toPath(), translation.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); log.println("Generated " + f.getAbsolutePath()); copyDirRec(srcFolder, dstFolder, log); } catch (Exception e) { log.println("Skipping generation of " + typeElt.getQualifiedName()); e.printStackTrace(log); } } } } } catch (Exception e) { e.printStackTrace(); } return true; } else { 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; } }