All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertx.docgen.processor.impl.BaseProcessor Maven / Gradle / Ivy

The newest version!
package io.vertx.docgen.processor.impl;

import com.sun.source.doctree.*;
import com.sun.source.doctree.ErroneousTree;
import com.sun.source.doctree.LiteralTree;
import com.sun.source.util.DocTreeScanner;
import com.sun.source.util.DocTrees;
import com.sun.source.util.TreePath;
import io.vertx.docgen.Document;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Julien Viet
 */
public abstract class BaseProcessor extends AbstractProcessor {

  private static final String DOCGEN_OUTPUT = "docgen.output", DOCGEN_EXTENSION = "docgen.extension",
    DOCGEN_SOURCE = "docgen.source", DOCGEN_SYNTAX = "docgen.syntax";

  protected DocTrees docTrees;
  protected Helper helper;
  protected List sources;
  protected Set postProcessors = new LinkedHashSet<>();
  protected Map resolutions = new HashMap<>();
  protected Syntax syntax;
  public Map failures = new HashMap<>();

  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.RELEASE_8;
  }

  @Override
  public Set getSupportedOptions() {
    return new HashSet<>(Arrays.asList(DOCGEN_OUTPUT, DOCGEN_EXTENSION, DOCGEN_SOURCE));
  }

  @Override
  public Set getSupportedAnnotationTypes() {
    return Collections.singleton("*");
  }

  public synchronized BaseProcessor registerPostProcessor(PostProcessor postProcessor) {
    if (getPostProcessor(postProcessor.getName()) != null) {
      throw new IllegalArgumentException("Post-processor with name '" + postProcessor.getName() + "' is already " +
          "registered.");
    }
    postProcessors.add(postProcessor);
    return this;
  }

  public synchronized PostProcessor getPostProcessor(String name) {
    for (PostProcessor pp : postProcessors) {
      if (pp.getName().equalsIgnoreCase(name)) {
        return pp;
      }
    }
    return null;
  }


  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    String sourceOpt = processingEnv.getOptions().get(DOCGEN_SOURCE);
    if (sourceOpt != null) {
      sources = new ArrayList<>(Arrays.asList(sourceOpt.split("\\s*,\\s*")));
    }
    docTrees = DocTrees.instance(processingEnv);
    helper = new Helper(processingEnv);
    String syntaxOpt = processingEnv.getOptions().get(DOCGEN_SYNTAX);
    this.syntax = "markdown".equals(syntaxOpt) ? Syntax.MARKDOWN : Syntax.ASCIIDOC;
    registerPostProcessor(new LanguageFilterPostProcessor());
  }

  private String render(List trees) {
    StringBuilder buffer = new StringBuilder();
    DocTreeVisitor visitor = new DocTreeScanner() {
      @Override
      public Void visitText(TextTree node, Void aVoid) {
        buffer.append(node.getBody());
        return super.visitText(node, aVoid);
      }
    };
    trees.forEach(tree -> tree.accept(visitor, null));
    return buffer.toString();
  }

  private final Map> state = new HashMap<>();

  @Override
  public boolean process(Set annotations, RoundEnvironment roundEnv) {
    StackTraceElement[] trace = Thread.currentThread().getStackTrace();
    for (StackTraceElement elt : trace) {
      if (elt.getClassName().startsWith("org.jetbrains")) {
        return true;
      }
    }
    if (failures.isEmpty()) {
      try {
        if (!roundEnv.processingOver()) {
          roundEnv.getElementsAnnotatedWith(Document.class).forEach(elt -> {
            try {
              PackageDoc doc = new PackageDoc((PackageElement) elt);
              state.put(doc, handleGen(doc));
            } catch (DocGenException e) {
              if (e.element == null) {
                e.element = elt;
              }
              throw e;
            }
          });

          if (sources != null && sources.size() > 0) {
            for (String source : sources) {

              // Handle wildcards
              List files = new ArrayList<>();
              File f = new File(source);
              if (!f.exists()) {
                if (f.getName().contains("*")) {
                  StringBuilder sb = new StringBuilder();
                  for (char c : f.getName().toCharArray()) {
                    if (c == '*') {
                      sb.append(".*");
                    } else {
                      sb.append(Matcher.quoteReplacement(Character.toString(c)));
                    }
                  }
                  Pattern p = Pattern.compile(sb.toString());
                  File parentFile = f.getParentFile();
                  File[] children = parentFile.listFiles();
                  if (children != null) {
                    for (File child : children) {
                      if (p.matcher(child.getName()).matches()) {
                        files.add(child);
                      }
                    }
                  }
                } else {
                  throw new FileNotFoundException("Cannot process document " + source);
                }
              } else {
                files.add(f);
              }
              for (File file : files) {
                if (file.isFile()) {
                  FileDoc fileDoc = new FileDoc(file, file.getName());
                  Map m = handleGen(fileDoc);
                  state.put(fileDoc, m);
                } else if (file.isDirectory()) {
                  Files.walk(file.toPath())
                    .map(Path::toFile)
                    .filter(File::isFile).forEach(docFile -> {
                    String relativePath = file.toPath().relativize(docFile.toPath()).toString();
                    FileDoc fileDoc = new FileDoc(docFile, relativePath);
                    Map m = handleGen(fileDoc);
                    state.put(fileDoc, m);
                  });
                } else {
                  throw new IOException("Document " + file.getAbsolutePath() + " is not a file nor a dir");
                }
              }
            }
            sources.clear();
          }

          Set processed = new HashSet<>();
          while (true) {
            Optional opt = resolutions
              .values()
              .stream()
              .filter(res -> res.elt == null && !processed.contains(res.signature))
              .findFirst();
            if (opt.isPresent()) {
              ElementResolution res = opt.get();
              processed.add(res.signature);
              res.tryResolve();
            } else {
              break;
            }
          }
        } else {
          state.forEach((doc, m) -> {
            m.forEach((gen, w) -> {
              String content = postProcess(gen.getName(), w.render());
              write(gen, doc, content);
            });
          });
        }
      } catch(Exception e) {
        Element reportedElt = (e instanceof DocGenException) ? ((DocGenException) e).element : null;
        String msg = e.getMessage();
        if (msg == null) {
          msg = e.toString();
        }
        e.printStackTrace();
        if (reportedElt != null) {
          processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, reportedElt);
          if (reportedElt instanceof PackageElement) {
            failures.put(((PackageElement) reportedElt).getQualifiedName().toString(), msg);
          } else {
            throw new UnsupportedOperationException("not implemented");
          }
        } else {
          processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg);
        }
      }
    }
    return false;
  }

  protected abstract Iterable generators();

  private Map handleGen(Doc doc) {
    Map map = new HashMap<>();
    for (DocGenerator generator : generators()) {
      generator.init(processingEnv);
      DocWriter writer = new DocWriter(doc instanceof FileDoc);
      doc.process(generator, writer);
      map.put(generator, writer);
    }
    return map;
  }

  /**
   * @return the extension obtained from processor option {@literal docgen.extension} defaults to {@literal .adoc}
   * when absent.
   */
  protected String getExtension() {
    String extension = processingEnv.getOptions().get(DOCGEN_EXTENSION);
    if (extension != null) {
      return extension;
    }
    return ".adoc";
  }

  protected String resolveLinkToPackageDoc(PackageElement elt) {
    Document annotation = elt.getAnnotation(Document.class);
    String fileName = annotation.fileName();
    if (fileName.isEmpty()) {
      return elt.toString() + getExtension();
    } else {
      return fileName;
    }
  }

  private URL getURL(JavaFileObject fileObject) {
    try {
      return fileObject.toUri().toURL();
    } catch (Exception e) {
      return null;
    }
  }

  /**
   * Resolve a label for the specified element, this is used when a link to a program element
   * does not specify an explicit label.

*

* Subclasses can override it to implement a particular behavior for elements. * * @param elt the elt to resolve a label for * @return the label */ private String resolveLabel(DocGenerator generator, Element elt) { String label = elt.getSimpleName().toString(); if (elt.getModifiers().contains(Modifier.STATIC) && (elt.getKind() == ElementKind.METHOD || elt.getKind() == ElementKind.FIELD)) { label = elt.getEnclosingElement().getSimpleName() + "." + label; } if (elt.getKind() == ElementKind.ANNOTATION_TYPE) { label = "@" +label; } return generator.resolveLabel(elt, label); } private final LinkedList stack = new LinkedList<>(); public abstract class Doc { public abstract String id(); abstract String resolveRelativeFileName(DocGenerator generator); protected final void process(DocGenerator generator, DocWriter writer) { if (this instanceof PackageDoc) { PackageElement pkgElt = ((PackageDoc) this).elt; for (PackageElement stackElt : stack) { if (pkgElt.getQualifiedName().equals(stackElt.getQualifiedName())) { throw new DocGenException(stack.peekLast(), "Circular include"); } } stack.addLast(pkgElt); String pkgSource = helper.readSource(pkgElt); TreePath pkgPath = docTrees.getPath(pkgElt); DocCommentTree docTree = docTrees.getDocCommentTree(pkgPath); DocTreeVisitor visitor = new DocTreeScanner() { private void copyContent(DocTree node) { int from = (int) docTrees.getSourcePositions().getStartPosition(pkgPath.getCompilationUnit(), docTree, node); int to = (int) docTrees.getSourcePositions().getEndPosition(pkgPath.getCompilationUnit(), docTree, node); writer.append(pkgSource, from, to); } @Override public Void visitUnknownBlockTag(UnknownBlockTagTree node, Void v) { writer.append("@").append(node.getTagName()).append(" "); return super.visitUnknownBlockTag(node, v); } @Override public Void visitDocComment(DocCommentTree node, Void v) { v = scan(node.getFirstSentence(), v); List body = node.getBody(); if (body.size() > 0) { writer.append("\n\n"); writer.resetParagraph(); v = scan(body, v); } List blockTags = node.getBlockTags(); if (blockTags.size() > 0) { writer.append("\n"); v = scan(blockTags, v); } return v; } @Override public Void visitErroneous(ErroneousTree node, Void v) { return visitText(node, v); } @Override public Void visitText(TextTree node, Void v) { String body = node.getBody(); helper.filterLang(body, generator.getName(), writer); return super.visitText(node, v); } /** * Handles both literal and code. We generate the asciidoc output using {@literal `}. */ @Override public Void visitLiteral(LiteralTree node, Void aVoid) { writer.append("`").append(node.getBody().getBody()).append("`"); return super.visitLiteral(node, aVoid); } @Override public Void visitEntity(EntityTree node, Void aVoid) { writer.append(EntityUtils.unescapeEntity(node.getName().toString())); return super.visitEntity(node, aVoid); } @Override public Void visitStartElement(StartElementTree node, Void v) { copyContent(node); return v; } @Override public Void visitEndElement(EndElementTree node, Void v) { writer.write("'); return v; } @Override public Void visitLink(LinkTree node, Void v) { String signature = node.getReference().getSignature(); String label = render(node.getLabel()).trim(); BaseProcessor.this.visitLink(pkgElt, label, signature, generator, writer); return v; } }; docTree.accept(visitor, null); stack.removeLast(); } else { FileDoc fileDoc = (FileDoc) this; try { String content = new String(Files.readAllBytes(fileDoc.file.toPath()), StandardCharsets.UTF_8); StringBuilder intermediate = new StringBuilder(content.length()); helper.filterLang(content, generator.getName(), intermediate); content = intermediate.toString(); Matcher linkMatcher = LINK_PATTERN.matcher(content); int prev = 0; while (linkMatcher.find()) { writer.write(content, prev, linkMatcher.start() - prev); String value = linkMatcher.group(1).trim(); Matcher methodLinkMatcher = METHOD_LINK_PATTERN.matcher(value); if (methodLinkMatcher.find()) { String signature = value.substring(0, methodLinkMatcher.end()); String label = value.substring(methodLinkMatcher.end()).trim(); writer.exec(() -> { BaseProcessor.this.visitLink(null, label, signature, generator, writer); }); } prev = linkMatcher.end(); } writer.append(content, prev, content.length()); } catch (IOException e) { throw new DocGenException(e.getMessage()); } } } } class PackageDoc extends Doc { final PackageElement elt; PackageDoc(PackageElement elt) { this.elt = elt; } @Override public String id() { return elt.getQualifiedName().toString(); } /** * Return the relative file name of a document. * * @param generator the doc generator * @return the relative file name */ public String resolveRelativeFileName(DocGenerator generator) { Document doc = elt.getAnnotation(Document.class); String relativeName = doc.fileName(); if (relativeName.isEmpty()) { relativeName = elt.getQualifiedName() + getExtension(); } return generator.resolveRelativeFileName(elt, relativeName); } } class FileDoc extends Doc { final File file; final String relativePath; FileDoc(File file, String relativePath) { this.file = file; this.relativePath = relativePath; } @Override public String id() { return relativePath; } @Override public String resolveRelativeFileName(DocGenerator generator) { return relativePath; } } private static final Pattern LINK_PATTERN = Pattern.compile("\\{@link\\s([^}]+)\\}"); private static final Pattern METHOD_LINK_PATTERN = Pattern.compile( "^([$_\\w]+\\.)*[$_\\w]+" + "(?:" + "#[$_\\w]+" + "(?:(?:\\([^)]*)\\)|$|(?= ))" + ")?"); private void visitLink(PackageElement pkgElt, String label, String signature, DocGenerator generator, DocWriter writer) { ElementResolution res = resolutions.get(signature); if (res == null) { res = new ElementResolution(signature); resolutions.put(signature, res); } LinkProcessing fut = new LinkProcessing(generator, label); res.add(fut); writer.write(() -> { DocWriter ww = fut.writer; if (ww == null) { throw new DocGenException(pkgElt, "Could not resolve " + signature); } return ww; }); } /** * The resolution of an element. */ class ElementResolution { final String signature; private Element elt; private List handlers = new ArrayList<>(); public ElementResolution(String signature) { this.signature = signature; } boolean tryResolve() { if (elt == null) { doResolve(); } return elt != null; } public boolean equals(Object o) { if (o instanceof ElementResolution) { ElementResolution that = (ElementResolution) o; return signature.equals(that.signature); } else { return false; } } @Override public int hashCode() { return signature.hashCode(); } private void doResolve() { elt = helper.resolveLink(signature); if (elt != null) { for (LinkProcessing fut : handlers) { fut.handle(elt); } handlers.clear(); } } private void add(LinkProcessing fut) { if (elt != null) { fut.handle(elt); } else { handlers.add(fut); } } } class LinkProcessing { final DocGenerator generator; final String label; private DocWriter writer; public LinkProcessing(DocGenerator generator, String label) { this.generator = generator; this.label = label; } void handle(Element elt) { writer = new DocWriter(); if (elt instanceof PackageElement) { PackageElement includedElt = (PackageElement) elt; if (includedElt.getAnnotation(Document.class) == null) { new PackageDoc(includedElt).process(generator, writer); } else { String link = resolveLinkToPackageDoc((PackageElement) elt); writer.append(link); } } else { if (helper.isExample(elt)) { String source = helper.readSource(elt); switch (elt.getKind()) { case CONSTRUCTOR: case METHOD: // Check whether or not the fragment must be translated String fragment; if (helper.hasToBeTranslated(elt)) { // Invoke the custom renderer, this may should the translation to the expected language. fragment = generator.renderSource((ExecutableElement) elt, source); } else { // Do not call the custom rendering process, just use the default / java one. JavaDocGenerator javaGen = new JavaDocGenerator(); javaGen.init(processingEnv); fragment = javaGen.renderSource((ExecutableElement) elt, source); } if (fragment != null) { writer.literalMode(); writer.append(fragment); writer.commentMode(); } return; case CLASS: case INTERFACE: case ENUM: case ANNOTATION_TYPE: TypeElement typeElt = (TypeElement) elt; JavaDocGenerator javaGen = new JavaDocGenerator(); javaGen.init(processingEnv); fragment = javaGen.renderSource(typeElt, source); if (fragment != null) { writer.literalMode(); writer.append(fragment); writer.commentMode(); } return; default: throw new UnsupportedOperationException("unsupported element: " + elt.getKind()); } } String link; switch (elt.getKind()) { case CLASS: case INTERFACE: case ANNOTATION_TYPE: case ENUM: { TypeElement typeElt = (TypeElement) elt; link = generator.resolveTypeLink(typeElt); break; } case METHOD: { ExecutableElement methodElt = (ExecutableElement) elt; TypeElement typeElt = (TypeElement) methodElt.getEnclosingElement(); link = generator.resolveMethodLink(methodElt); break; } case CONSTRUCTOR: { ExecutableElement constructorElt = (ExecutableElement) elt; TypeElement typeElt = (TypeElement) constructorElt.getEnclosingElement(); link = generator.resolveConstructorLink(constructorElt); break; } case FIELD: case ENUM_CONSTANT: { VariableElement variableElt = (VariableElement) elt; TypeElement typeElt = (TypeElement) variableElt.getEnclosingElement(); link = generator.resolveFieldLink(variableElt); break; } default: throw new UnsupportedOperationException("Not yet implemented " + elt + " with kind " + elt.getKind()); } String s; if (label.length() == 0) { s = resolveLabel(generator, elt); } else { s = label; } if (link != null) { syntax.writeLink(link, s, writer); } else { writer.append("`").append(s).append("`"); } } } } protected String postProcess(String name, String content) { String processed = applyVariableSubstitution(content); processed = applyPostProcessors(name, processed); return processed; } protected void write(DocGenerator generator, Doc doc, String content) { String outputOpt = processingEnv.getOptions().get(DOCGEN_OUTPUT); if (outputOpt != null) { outputOpt = outputOpt.replace("$lang", generator.getName()); String relativeName = doc.resolveRelativeFileName(generator); try { File dir = new File(outputOpt); for (int i = relativeName.indexOf('/'); i != -1; i = relativeName.indexOf('/', i + 1)) { dir = new File(dir, relativeName.substring(0, i)); relativeName = relativeName.substring(i + 1); } File file = new File(dir, relativeName); ensureDir(file.getParentFile()); try (FileWriter writer = new FileWriter(file)) { writer.write(content); } } catch (IOException e) { e.printStackTrace(); } } } /** * Apply post-processors. * * @param content the (asciidoc) content * @return the content after post-processing. */ protected String applyPostProcessors(String name2, String content) { final List lines = Arrays.asList(content.split("\r?\n")); StringBuilder processed = new StringBuilder(); Iterator iterator = lines.iterator(); while (iterator.hasNext()) { String line = iterator.next(); String trimmedLine = line.trim(); if (!PostProcessor.isBlockDeclaration(trimmedLine)) { processed.append(line); if (iterator.hasNext()) { processed.append("\n"); } } else { String name = PostProcessor.getProcessorName(trimmedLine); String[] attributes = PostProcessor.getProcessorAttributes(trimmedLine); PostProcessor postProcessor = getPostProcessor(name); if (postProcessor == null) { processed.append(line); if (iterator.hasNext()) { processed.append("\n"); } } else { // Extract content. String block = PostProcessor.getBlockContent(iterator); processed.append(postProcessor.process(name2, block, attributes)); if (iterator.hasNext()) { processed.append("\n"); } } } } return processed.toString(); } private void ensureDir(File dir) { if (dir.exists()) { if (!dir.isDirectory()) { throw new DocGenException("File " + dir.getAbsolutePath() + " is not a dir"); } } else if (!dir.mkdirs()) { throw new DocGenException("could not create dir " + dir.getAbsolutePath()); } } /** * Replace `@{var} by the variable value passed to the annotation processor. * * @param content the content * @return the content with variable values */ public String applyVariableSubstitution(String content) { for (Map.Entry entry : processingEnv.getOptions().entrySet()) { content = content.replace("${" + entry.getKey() + "}", entry.getValue()); } return content; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy