Configure ASCIIDOCTOR formatter

Set options and attributes for asciidoc generation, include CSS and
set the safe mode to SECURE.

A new configuration parameter allows to control whether a Table Of
Contents should be included at the beginning of each document. For now
this is a global configuration, but a follow-up change should make
this configurable per project.

Change-Id: I51faf84e8edd6a6e515e8f2693acd5d45929501f
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
index cbb6201..fb791d6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
@@ -25,6 +25,7 @@
   public static final String KEY_ALLOW_HTML = "allowHtml";
   public static final String KEY_ENABLED = "enabled";
   public static final String KEY_EXT = "ext";
+  public static final String KEY_INCLUDE_TOC = "includeToc";
   public static final String KEY_MIME_TYPE = "mimeType";
   public static final String KEY_PREFIX = "prefix";
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
index 99f5792..758a7c2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
@@ -38,6 +38,7 @@
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -103,10 +104,16 @@
               && RawText.isBinary(bytes)) {
             return Resources.METHOD_NOT_ALLOWED;
           }
-          String html =
-              formatter.get().format(key.getProject().get(), formatterCfg,
-                  replaceMacros(key.getProject(), bytes));
-          return getAsHtmlResource(html, commit.getCommitTime());
+          ObjectReader reader = repo.newObjectReader();
+          try {
+            String html =
+                formatter.get().format(key.getProject().get(),
+                    reader.abbreviate(key.getRevId()).name(), formatterCfg,
+                    replaceMacros(key.getProject(), bytes));
+            return getAsHtmlResource(html, commit.getCommitTime());
+          } finally {
+            reader.release();
+          }
         } finally {
           tw.release();
         }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/AsciidoctorFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/AsciidoctorFormatter.java
index d15ab42..5517fbe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/AsciidoctorFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/AsciidoctorFormatter.java
@@ -14,20 +14,133 @@
 
 package com.googlesource.gerrit.plugins.xdocs.formatter;
 
+import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_INCLUDE_TOC;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
 import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
 
 import org.asciidoctor.Asciidoctor;
+import org.asciidoctor.Attributes;
+import org.asciidoctor.AttributesBuilder;
+import org.asciidoctor.Options;
+import org.asciidoctor.OptionsBuilder;
+import org.asciidoctor.SafeMode;
+import org.eclipse.jgit.util.TemporaryBuffer;
 
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.util.HashMap;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Properties;
 
+@Singleton
 public class AsciidoctorFormatter implements Formatter {
-  public final static String NAME = "ASCIIDOCTOR";
+  public static final String NAME = "ASCIIDOCTOR";
+
+  private static final String BACKEND = "html5";
+  private static final String DOCTYPE = "article";
+  private static final String ERUBY = "erb";
+
+  private final File baseDir;
+  private final String css;
+  private final Properties attributes;
+
+  @Inject
+  public AsciidoctorFormatter(@PluginData File baseDir) throws IOException {
+    this.baseDir = baseDir;
+    this.css = readCss();
+    this.attributes = readAttributes();
+  }
 
   @Override
-  public String format(String projectName, ConfigSection cfg, String raw)
-      throws IOException {
-    return Asciidoctor.Factory.create(AsciidoctorFormatter.class.getClassLoader())
-        .convert(raw, new HashMap<String, Object>());
+  public String format(String projectName, String revision, ConfigSection cfg,
+      String raw) throws IOException {
+    // asciidoctor ignores all attributes if no output file is specified,
+    // this is why we must specify an output file and then read its content
+    File tmpFile =
+        new File(baseDir, "tmp/asciidoctor-" + TimeUtil.nowTs().getNanos() + ".tmp");
+    try {
+      Asciidoctor.Factory.create(AsciidoctorFormatter.class.getClassLoader())
+          .render(raw, createOptions(cfg, revision, tmpFile));
+      try (FileInputStream input = new FileInputStream(tmpFile)) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteStreams.copy(input, out);
+        return insertCss(out.toString(UTF_8.name()));
+      }
+    } finally {
+      if (!tmpFile.delete()) {
+        tmpFile.deleteOnExit();
+      }
+    }
+  }
+
+  private Options createOptions(ConfigSection cfg, String revision, File out) {
+    return OptionsBuilder.options()
+        .backend(BACKEND)
+        .docType(DOCTYPE)
+        .eruby(ERUBY)
+        .safe(SafeMode.SECURE)
+        .attributes(getAttributes(cfg, revision))
+        .mkDirs(true)
+        .toFile(out)
+        .get();
+  }
+
+  private Attributes getAttributes(ConfigSection cfg, String revision) {
+    AttributesBuilder ab = AttributesBuilder.attributes()
+        .tableOfContents(cfg.getBoolean(KEY_INCLUDE_TOC, true))
+        .sourceHighlighter("prettify");
+    for (String name : attributes.stringPropertyNames()) {
+      ab.attribute(name, attributes.getProperty(name));
+    }
+    ab.attribute("last-update-label!");
+    ab.attribute("revnumber", revision);
+    return ab.get();
+  }
+
+  private String insertCss(String html) {
+    int p = html.lastIndexOf("</head>");
+    if (p > 0) {
+      StringBuilder b = new StringBuilder();
+      b.append(html.substring(0, p));
+      b.append("<style type=\"text/css\">\n");
+      b.append(css);
+      b.append("</style>\n");
+      b.append(html.substring(p));
+      return b.toString();
+    } else {
+      return html;
+    }
+  }
+
+  private static String readCss() throws IOException {
+    String name = "asciidoctor.css";
+    URL url = AsciidoctorFormatter.class.getResource(name);
+    if (url == null) {
+      throw new FileNotFoundException("Resource " + name);
+    }
+    try (InputStream in = url.openStream();
+        TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024)) {
+      tmp.copy(in);
+      return new String(tmp.toByteArray(), UTF_8);
+    }
+  }
+
+  private static Properties readAttributes() throws IOException {
+    Properties attributes = new Properties();
+    try (InputStream in = AsciidoctorFormatter.class
+        .getResourceAsStream("asciidoctor.properties")) {
+      attributes.load(in);
+    }
+    return attributes;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java
index 9432983..f58efee 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java
@@ -28,11 +28,13 @@
    *
    * @param projectName the name of the project that contains the file to be
    *        formatted
+   * @param revision the abbreviated revision from which the file is loaded
    * @param cfg the configuration for this formatter
    * @param raw the raw text
    * @return the given text formatted as html
    * @throws IOException thrown if the formatting fails
    */
-  public String format(String projectName, ConfigSection cfg, String raw)
+  public String format(String projectName, String revision, ConfigSection cfg,
+      String raw)
       throws IOException;
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java
index 3fa1b64..0f2b2f3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java
@@ -52,8 +52,8 @@
   }
 
   @Override
-  public String format(String projectName, ConfigSection cfg, String raw)
-      throws UnsupportedEncodingException {
+  public String format(String projectName, String revision, ConfigSection cfg,
+      String raw) throws UnsupportedEncodingException {
     com.google.gerrit.server.documentation.MarkdownFormatter f =
         new com.google.gerrit.server.documentation.MarkdownFormatter();
     if (!cfg.getBoolean(KEY_ALLOW_HTML, false)) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java
index 625d422..0c79bd5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java
@@ -22,7 +22,8 @@
   public final static String NAME = "PLAIN_TEXT";
 
   @Override
-  public String format(String projectName, ConfigSection cfg, String raw) {
+  public String format(String projectName, String revision, ConfigSection cfg,
+      String raw) {
     return "<pre>" + escapeHtml(raw) + "</pre>";
   }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 83b56cc..c18c697 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -145,3 +145,13 @@
 	*CANNOT* be overridden on project-level.
 
 	Default: `true`
+
+<a id="formatterIncludeToc">
+formatter.<formatter>.includeToc
+:	Whether a Table Of Contents should be included into each document.
+
+	*CANNOT* be overridden on project-level.
+
+	Supported for the following formatters: `ASCIIDOCTOR`
+
+	Default: `true`
diff --git a/src/main/resources/com/googlesource/gerrit/plugins/xdocs/formatter/asciidoctor.css b/src/main/resources/com/googlesource/gerrit/plugins/xdocs/formatter/asciidoctor.css
new file mode 100644
index 0000000..c49b596
--- /dev/null
+++ b/src/main/resources/com/googlesource/gerrit/plugins/xdocs/formatter/asciidoctor.css
@@ -0,0 +1,54 @@
+body {
+  margin: 1em auto;
+  width: 900px;
+}
+
+#toctitle {
+  margin-top: 0.5em;
+  font-weight: bold;
+}
+
+h1, h2, h3, h4, h5, h6, #toctitle {
+  color: #527bbd;
+  font-family: sans-serif;
+}
+
+h1, h2, h3 {
+  border-bottom: 2px solid silver;
+}
+
+p {
+  margin: 0.5em 0 0.5em 0;
+}
+li p {
+  margin: 0.2em 0 0.2em 0;
+}
+
+.listingblock > .content {
+  border: 2px solid silver;
+  background: #ebebeb;
+  margin-left: 2em;
+  color: darkgreen;
+  padding: 2px;
+  overflow: auto;
+}
+
+.listingblock > .content pre {
+  background: none;
+  border: 0 solid silver;
+  padding: 0 0 0 0;
+}
+
+dl dt {
+  margin-top: 1em;
+}
+
+table.tableblock {
+  border-collapse: collapse;
+}
+
+table.tableblock,
+th.tableblock,
+td.tableblock {
+  border: 1px solid #EEE;
+}
diff --git a/src/main/resources/com/googlesource/gerrit/plugins/xdocs/formatter/asciidoctor.properties b/src/main/resources/com/googlesource/gerrit/plugins/xdocs/formatter/asciidoctor.properties
new file mode 100644
index 0000000..9509f08
--- /dev/null
+++ b/src/main/resources/com/googlesource/gerrit/plugins/xdocs/formatter/asciidoctor.properties
@@ -0,0 +1,7 @@
+newline = \n
+asterisk = &#42;
+plus = &#43;
+caret = &#94;
+startsb = &#91;
+endsb = &#93;
+tilde = &#126;