package io.vertx.codegen;
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.annotations.DataObject;
import io.vertx.codegen.annotations.ModuleGen;
import io.vertx.codegen.annotations.ProxyGen;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.codegen.type.ClassKind;
import io.vertx.codegen.type.TypeNameTranslator;
import org.mvel2.MVEL;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.FilerException;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
*/
@javax.annotation.processing.SupportedOptions({"codegen.output","codegen.generators"})
@javax.annotation.processing.SupportedSourceVersion(javax.lang.model.SourceVersion.RELEASE_8)
public class CodeGenProcessor extends AbstractProcessor {
private static final int JAVA= 0, RESOURCE = 1, OTHER = 2;
private final static ObjectMapper mapper = new ObjectMapper();
private static final Logger log = Logger.getLogger(CodeGenProcessor.class.getName());
private File outputDirectory;
private List<CodeGenerator> codeGenerators;
private Map<String, GeneratedFile> generatedFiles = new HashMap<>();
private Map<String, GeneratedFile> generatedResources = new HashMap<>();
private Map<String, String> relocations = new HashMap<>();
@Override
public Set<String> getSupportedAnnotationTypes() {
return Arrays.asList(
VertxGen.class,
ProxyGen.class,
DataObject.class,
DataObject.class,
ModuleGen.class
).stream().map(Class::getName).collect(Collectors.toSet());
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
generatedFiles.clear();
generatedResources.clear();
}
protected Predicate<CodeGenerator> filterGenerators() {
String generatorsOption = processingEnv.getOptions().get("codegen.generators");
if (generatorsOption == null) {
generatorsOption = processingEnv.getOptions().get("codeGenerators");
if (generatorsOption != null) {
log.warning("Please use 'codegen.generators' option instead of 'codeGenerators' option");
}
}
if (generatorsOption != null) {
List<Pattern> wanted = Stream.of(generatorsOption.split(","))
.map(String::trim)
.map(Pattern::compile)
.collect(Collectors.toList());
return cg -> wanted.stream()
.filter(p -> p.matcher(cg.name).matches())
.findFirst()
.isPresent();
} else {
return null;
}
}
protected List<CodeGenerator> loadGenerators() {
List<CodeGenerator> generators = new ArrayList<>();
Enumeration<URL> descriptors = Collections.emptyEnumeration();
try {
descriptors = CodeGenProcessor.class.getClassLoader().getResources("codegen.json");
} catch (IOException ignore) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Could not load code generator descriptors");
}
Set<String> templates = new HashSet<>();
while (descriptors.hasMoreElements()) {
URL descriptor = descriptors.nextElement();
try (Scanner scanner = new Scanner(descriptor.openStream(), "UTF-8").useDelimiter("\\A")) {
String s = scanner.next();
ObjectNode obj = (ObjectNode) mapper.readTree(s);
String name = obj.get("name").asText();
ArrayNode generatorsCfg = (ArrayNode) obj.get("generators");
for (JsonNode generator : generatorsCfg) {
String kind = generator.get("kind").asText();
JsonNode templateFilenameNode = generator.get("templateFilename");
if (templateFilenameNode == null) {
templateFilenameNode = generator.get("templateFileName");
}
String templateFilename = templateFilenameNode.asText();
JsonNode filenameNode = generator.get("filename");
if (filenameNode == null) {
filenameNode = generator.get("fileName");
}
String filename = filenameNode.asText();
boolean incremental = generator.has("incremental") && generator.get("incremental").asBoolean();
if (!templates.contains(templateFilename)) {
templates.add(templateFilename);
generators.add(new CodeGenerator(name, kind, incremental, filename, templateFilename));
}
}
} catch (Exception e) {
String msg = "Could not load code generator " + descriptor;
log.log(Level.SEVERE, msg, e);
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg);
}
}
return generators;
}
private Collection<CodeGenerator> getCodeGenerators() {
if (codeGenerators == null) {
String outputDirectoryOption = processingEnv.getOptions().get("codegen.output");
if (outputDirectoryOption == null) {
outputDirectoryOption = processingEnv.getOptions().get("outputDirectory");
if (outputDirectoryOption != null) {
log.warning("Please use 'codegen.output' option instead of 'outputDirectory' option");
}
}
if (outputDirectoryOption != null) {
outputDirectory = new File(outputDirectoryOption);
if (!outputDirectory.exists()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Output directory " + outputDirectoryOption + " does not exist");
}
if (!outputDirectory.isDirectory()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Output directory " + outputDirectoryOption + " is not a directory");
}
}
List<CodeGenerator> generators = loadGenerators();
Predicate<CodeGenerator> filter = filterGenerators();
if (filter != null) {
generators = generators.stream().filter(filter).collect(Collectors.toList());
}
generators.forEach(gen -> {
Template template = new Template(gen.templateFilename);
template.setOptions(processingEnv.getOptions());
gen.template = template;
gen.filenameExpr = MVEL.compileExpression(gen.filename);
log.info("Loaded " + gen.name + " code generator");
});
relocations = processingEnv.getOptions()
.entrySet()
.stream()
.filter(e -> e.getKey().startsWith("codegen.output."))
.collect(Collectors.toMap(
e -> e.getKey().substring("codegen.output.".length()),
Map.Entry::getValue)
);
codeGenerators = generators;
}
return codeGenerators;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
Collection<CodeGenerator> codeGenerators = getCodeGenerators();
if (!roundEnv.errorRaised()) {
CodeGen codegen = new CodeGen(processingEnv, roundEnv);
Map<String, GeneratedFile> generatedClasses = new HashMap<>();
// Generate source code
codegen.getModels().forEach(entry -> {
try {
Model model = entry.getValue();
Map<String, Object> vars = new HashMap<>();
vars.put("helper", new Helper());
vars.put("options", processingEnv.getOptions());
vars.put("fileSeparator", File.separator);
vars.put("fqn", model.getFqn());
vars.put("module", model.getModule());
vars.put("model", model);
vars.putAll(model.getVars());
vars.putAll(ClassKind.vars());
vars.putAll(MethodKind.vars());
vars.putAll(Case.vars());
for (CodeGenerator codeGenerator : codeGenerators) {
vars.putAll(TypeNameTranslator.vars(codeGenerator.name));
if (codeGenerator.kind.equals(model.getKind())) {
String relativeName = (String) MVEL.executeExpression(codeGenerator.filenameExpr, vars);
if (relativeName != null) {
int kind;
if (relativeName.endsWith(".java") && !relativeName.contains("/")) {
String relocation = relocations.get(codeGenerator.name);
if (relocation != null) {
kind = OTHER;
relativeName = relocation + '/' +
relativeName.substring(0, relativeName.length() - ".java".length()).replace('.', '/') + ".java";
} else {
kind = JAVA;
}
} else if (relativeName.startsWith("resources/")) {
kind = RESOURCE;
} else {
kind = OTHER;
}
if (kind == JAVA) {
// Special handling for .java
String fqn = relativeName.substring(0, relativeName.length() - ".java".length());
// Avoid to recreate the same file (this may happen as we unzip and recompile source trees)
if (processingEnv.getElementUtils().getTypeElement(fqn) != null) {
continue;
}
List<ModelProcessing> processings = generatedClasses.computeIfAbsent(fqn, GeneratedFile::new);
processings.add(new ModelProcessing(model, codeGenerator));
} else if (kind == RESOURCE) {
relativeName = relativeName.substring("resources/".length());
List<ModelProcessing> processings = generatedResources.computeIfAbsent(relativeName, GeneratedFile::new);
processings.add(new ModelProcessing(model, codeGenerator));
} else {
List<ModelProcessing> processings = generatedFiles.computeIfAbsent(relativeName, GeneratedFile::new);
processings.add(new ModelProcessing(model, codeGenerator));
}
}
}
}
} catch (GenException e) {
reportGenException(e);
} catch (Exception e) {
reportException(e, entry.getKey());
}
});
// Generate classes
generatedClasses.values().forEach(generated -> {
try {
String content = generated.generate();
if (content.length() > 0) {
JavaFileObject target = processingEnv.getFiler().createSourceFile(generated.uri);
try (Writer writer = target.openWriter()) {
writer.write(content);
}
log.info("Generated model " + generated.get(0).model.getFqn() + ": " + generated.uri);
}
} catch (GenException e) {
reportGenException(e);
} catch (Exception e) {
reportException(e, generated.get(0).model.getElement());
}
});
}
} else {
// Generate resources
for (GeneratedFile generated : generatedResources.values()) {
try {
String content = generated.generate();
if (content.length() > 0) {
try (Writer w = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", generated.uri).openWriter()) {
w.write(content);
}
boolean createSource;
try {
processingEnv.getFiler().getResource(StandardLocation.SOURCE_OUTPUT, "", generated.uri);
createSource = true;
} catch (FilerException e) {
// SOURCE_OUTPUT == CLASS_OUTPUT
createSource = false;
}
if (createSource) {
try (Writer w = processingEnv.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, "", generated.uri).openWriter()) {
w.write(content);
}
}
log.info("Generated model " + generated.get(0).model.getFqn() + ": " + generated.uri);
}
} catch (GenException e) {
reportGenException(e);
} catch (Exception e) {
reportException(e, generated.get(0).model.getElement());
}
}
// Generate files
if (outputDirectory != null) {
generatedFiles.values().forEach(generated -> {
File file = new File(outputDirectory, generated.uri);
Helper.ensureParentDir(file);
String content = generated.generate();
if (content.length() > 0) {
try (FileWriter fileWriter = new FileWriter(file)) {
fileWriter.write(content);
} catch (GenException e) {
reportGenException(e);
} catch (Exception e) {
reportException(e, generated.get(0).model.getElement());
}
log.info("Generated model " + generated.get(0).model.getFqn() + ": " + generated.uri);
}
});
}
}
return true;
}
private void reportGenException(GenException e) {
String msg = "Could not generate model for " + e.element + ": " + e.msg;
log.log(Level.SEVERE, msg, e);
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, e.element);
}
private void reportException(Exception e, Element elt) {
String msg = "Could not generate element for " + elt + ": " + e.getMessage();
log.log(Level.SEVERE, msg, e);
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, elt);
}
private static class ModelProcessing {
final Model model;
final CodeGenerator generator;
public ModelProcessing(Model model, CodeGenerator generator) {
this.model = model;
this.generator = generator;
}
}
private static class GeneratedFile extends ArrayList<ModelProcessing> {
final String uri;
final Map<String, Object> session = new HashMap<>();
public GeneratedFile(String uri) {
super();
this.uri = uri;
}
@Override
public boolean add(ModelProcessing modelProcessing) {
if (!modelProcessing.generator.incremental) {
clear();
}
return super.add(modelProcessing);
}
String generate() {
Collections.sort(this, (o1, o2) ->
o1.model.getElement().getSimpleName().toString().compareTo(
o2.model.getElement().getSimpleName().toString()));
int index = 0;
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < size(); i++) {
ModelProcessing processing = get(i);
Map<String, Object> vars = new HashMap<>();
vars.putAll(TypeNameTranslator.vars(processing.generator.name));
if (processing.generator.incremental) {
vars.put("incrementalIndex", index++);
vars.put("incrementalSize", size());
vars.put("session", session);
}
try {
String part = processing.generator.template.render(processing.model, vars);
if (part != null) {
buffer.append(part);
}
} catch (GenException e) {
throw e;
} catch (Exception e) {
throw new GenException(processing.model.getElement(), e.getMessage());
}
}
return buffer.toString();
}
}
}