Do all asciidoctor rendering in a single jvm

Previously for every html rendered, there's a separate jvm used, and buck's
parallel building will execute multiple jvm at once, burning the CPU and being
very slow.

Modified the asciidoctor-java-integrator CLI java wrapper interface to do that
all in a single jvm.

On a workstation tested, "buck build Documentation:html" took about 140s before,
and 60s after.

As a side-effect, we lose the build rules of the single html files (e.g.
"buck build Documentation:licenses.html")

Change-Id: Ifc70c63676b59571c6e240636752b7cba9270f04
diff --git a/Documentation/BUCK b/Documentation/BUCK
index faa2553..2876f13 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -4,48 +4,41 @@
 
 MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
 SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
-HTML = [txt[0:-4] + '.html' for txt in SRCS]
 
 genrule(
   name = 'html',
   cmd = 'cd $TMP;' +
     'mkdir -p Documentation/images;' +
+    'unzip -q $SRCDIR/only_html.zip -d Documentation/;' +
     'for s in $SRCS;do ln -s $s Documentation;done;' +
     'mv Documentation/*.{jpg,png} Documentation/images;' +
+    'rm Documentation/only_html.zip;' +
     'rm Documentation/licenses.txt;' +
     'cp $SRCDIR/licenses.txt LICENSES.txt;' +
     'zip -qr $OUT *',
-  srcs = [genfile(d) for d in HTML] +
+  srcs = [genfile('only_html.zip')] +
     glob([
       'images/*.jpg',
       'images/*.png',
     ]) + [
-    genfile('doc.css'),
-    genfile('licenses.html'),
+    'doc.css',
     genfile('licenses.txt'),
   ],
-  deps = [':' + d for d in HTML] + [
-    ':licenses.html',
+  deps = [
+    ':generate_html',
     ':licenses.txt',
-    ':doc.css',
   ],
   out = 'html.zip',
   visibility = ['PUBLIC'],
 )
 
-genrule(
-  name = 'doc.css',
-  cmd = 'ln -s $SRCDIR/doc.css $OUT',
-  srcs = ['doc.css'],
-  out = 'doc.css',
-)
-
 genasciidoc(
+  name = 'generate_html',
   srcs = SRCS + [genfile('licenses.txt')],
-  outs = HTML + ['licenses.html'],
-  deps = DOCUMENTATION_DEPS,
+  deps = [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
+  out = 'only_html.zip',
 )
 
 genrule(
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index 8279847..e2de785 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -13,25 +13,30 @@
 # limitations under the License.
 
 def genasciidoc(
+    name,
+    out,
     srcs = [],
-    outs = [],
-    deps = {},
+    deps = [],
     attributes = [],
     backend = None,
     visibility = []):
   EXPN = '.expn'
 
-  asciidoc = ['$(exe //lib/asciidoctor:asciidoc)']
+  asciidoc = [
+      '$(exe //lib/asciidoctor:asciidoc)',
+      '-z', '$OUT',
+      '--in-ext', '".txt%s"' % EXPN,
+      '--out-ext', '".html"',
+  ]
   if backend:
     asciidoc.extend(['-b', backend])
   for attribute in attributes:
     asciidoc.extend(['-a', attribute])
-  asciidoc.extend(['-o', '$OUT'])
+  asciidoc.append('$SRCS')
+  newsrcs = []
+  newdeps = deps + ['//lib/asciidoctor:asciidoc']
 
-  for p in zip(srcs, outs):
-    src, out = p
-    dep = deps.get(src) or []
-
+  for src in srcs:
     tx = []
     fn = src
     if fn.startswith('BUCKGEN:') :
@@ -48,14 +53,14 @@
       deps = tx + [':replace_macros'],
       out = ex,
     )
-    genrule(
-      name = out,
-      cmd = ' '.join(asciidoc + ['$SRCDIR/' + ex]),
-      srcs = [genfile(ex)] + [genfile(n + EXPN) for n in dep],
-      deps = [':' + n + EXPN for n in dep] + [
-        ':' + ex,
-        '//lib/asciidoctor:asciidoc',
-      ],
-      out = out,
-      visibility = visibility,
-    )
+    newdeps.append(':' + ex)
+    newsrcs.append(genfile(ex))
+
+  genrule(
+    name = name,
+    cmd = ' '.join(asciidoc),
+    srcs = newsrcs,
+    deps = newdeps,
+    out = out,
+    visibility = visibility,
+  )
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index 25b9eb8..6a1fdd7 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -2,11 +2,21 @@
 
 java_binary(
   name = 'asciidoc',
-  main_class = 'org.asciidoctor.cli.AsciidoctorInvoker',
-  deps = [':asciidoctor'],
+  main_class = 'Main',
+  deps = [':main_lib'],
   visibility = ['PUBLIC'],
 )
 
+java_library(
+  name = 'main_lib',
+  srcs = ['java/Main.java'],
+  deps = [
+    ':asciidoctor',
+    ':jruby',
+    '//lib:args4j',
+  ],
+)
+
 maven_jar(
   name = 'asciidoctor',
   id = 'org.asciidoctor:asciidoctor-java-integration:0.1.3',
@@ -14,19 +24,6 @@
   license = 'Apache2.0',
   visibility = [],
   attach_source = False,
-  deps = [
-    ':jcommander',
-    ':jruby',
-  ],
-)
-
-maven_jar(
-  name = 'jcommander',
-  id = 'com.beust:jcommander:1.30',
-  sha1 = 'c440b30a944ba199751551aee393f8aa03b3c327',
-  license = 'Apache2.0',
-  visibility = [],
-  attach_source = False,
 )
 
 maven_jar(
diff --git a/lib/asciidoctor/java/Main.java b/lib/asciidoctor/java/Main.java
new file mode 100644
index 0000000..eb1ef33
--- /dev/null
+++ b/lib/asciidoctor/java/Main.java
@@ -0,0 +1,159 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.asciidoctor.Asciidoctor;
+import org.asciidoctor.AttributesBuilder;
+import org.asciidoctor.Options;
+import org.asciidoctor.OptionsBuilder;
+import org.asciidoctor.internal.JRubyAsciidoctor;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+public class Main {
+
+  private static final int BUFSIZ = 4096;
+  private static final String DOCTYPE = "article";
+  private static final String ERUBY = "erb";
+
+  @Option(name = "-b", usage = "set output format backend")
+  private String backend = "html5";
+
+  @Option(name = "-z", usage = "output zip file")
+  private String zipFile;
+
+  @Option(name = "--in-ext", usage = "extension for input files")
+  private String inExt = ".txt";
+
+  @Option(name = "--out-ext", usage = "extension for output files")
+  private String outExt = ".html";
+
+  @Option(name = "-a", usage =
+      "a list of attributes, in the form key or key=value pair")
+  private List<String> attributes = new ArrayList<String>();
+
+  @Argument(usage = "input files")
+  private List<String> inputFiles = new ArrayList<String>();
+
+  private String mapInFileToOutFile(String inFile) {
+    String basename = new File(inFile).getName();
+    if (basename.endsWith(inExt)) {
+      basename = basename.substring(0, basename.length() - inExt.length());
+    } else {
+      // Strip out the last extension
+      int pos = basename.lastIndexOf('.');
+      if (pos > 0) {
+        basename = basename.substring(0, pos);
+      }
+    }
+    return basename + outExt;
+  }
+
+  private Options createOptions(File tmpFile) {
+    OptionsBuilder optionsBuilder = OptionsBuilder.options();
+
+    optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY);
+    // XXX(fishywang): ideally we should just output to a string and add the
+    // content into zip. But asciidoctor will actually ignore all attributes if
+    // not output to a file. So we *have* to output to a file then read the
+    // content of the file into zip.
+    optionsBuilder.toFile(tmpFile);
+
+    AttributesBuilder attributesBuilder = AttributesBuilder.attributes();
+    attributesBuilder.attributes(getAttributes());
+    optionsBuilder.attributes(attributesBuilder.get());
+
+    return optionsBuilder.get();
+  }
+
+  private Map<String, Object> getAttributes() {
+    Map<String, Object> attributeValues = new HashMap<String, Object>();
+
+    for (String attribute : attributes) {
+      int equalsIndex = attribute.indexOf('=');
+      if(equalsIndex > -1) {
+        String name = attribute.substring(0, equalsIndex);
+        String value = attribute.substring(equalsIndex + 1, attribute.length());
+
+        attributeValues.put(name, value);
+      } else {
+        attributeValues.put(attribute, "");
+      }
+    }
+
+    return attributeValues;
+  }
+
+  private void invoke(String... parameters) throws IOException {
+    CmdLineParser parser = new CmdLineParser(this);
+    try {
+      parser.parseArgument(parameters);
+      if (inputFiles.isEmpty()) {
+        throw new CmdLineException(parser,
+            "asciidoctor: FAILED: input file missing");
+      }
+    } catch (CmdLineException e) {
+      System.err.println(e.getMessage());
+      parser.printUsage(System.err);
+      System.exit(1);
+      return;
+    }
+
+    ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile));
+    byte[] buf = new byte[BUFSIZ];
+    for (String inputFile : inputFiles) {
+      File tmp = File.createTempFile("doc", ".html");
+      Options options = createOptions(tmp);
+      renderInput(options, inputFile);
+
+      FileInputStream input = new FileInputStream(tmp);
+      int len;
+      zip.putNextEntry(new ZipEntry(mapInFileToOutFile(inputFile)));
+      while ((len = input.read(buf)) > 0) {
+        zip.write(buf, 0, len);
+      }
+      input.close();
+      tmp.delete();
+      zip.closeEntry();
+    }
+    zip.close();
+  }
+
+  private void renderInput(Options options, String inputFile) {
+    Asciidoctor asciidoctor = JRubyAsciidoctor.create();
+    asciidoctor.renderFile(new File(inputFile), options);
+  }
+
+  public static void main(String[] args) {
+    try {
+      new Main().invoke(args);
+    } catch (IOException e) {
+      System.err.println(e.getMessage());
+      System.exit(1);
+    }
+  }
+}