Merge "Add navigation links to side-by-side and unified diff screens"
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/Module.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/Module.java
index 65be419..d822faf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/Module.java
@@ -31,6 +31,7 @@
 import com.googlesource.gerrit.plugins.xdocs.formatter.Formatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.MarkdownFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.PlainTextFormatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.ZipFormatter;
 
 import java.util.List;
 
@@ -57,6 +58,9 @@
     bind(Formatter.class)
         .annotatedWith(Exports.named(PlainTextFormatter.NAME))
         .to(PlainTextFormatter.class);
+    bind(Formatter.class)
+        .annotatedWith(Exports.named(ZipFormatter.NAME))
+        .to(ZipFormatter.class);
 
     DynamicSet.bind(binder(), ProjectWebLink.class)
         .to(XDocWebLink.class);
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 e84e1e4..1f86e69 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
@@ -17,9 +17,12 @@
 import com.googlesource.gerrit.plugins.xdocs.formatter.AsciidoctorFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.MarkdownFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.PlainTextFormatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.ZipFormatter;
 
 import org.eclipse.jgit.lib.Config;
 
+import java.util.Arrays;
+
 public class XDocGlobalConfig {
   public static final String SECTION_FORMATTER = "formatter";
   public static final String KEY_ALLOW_HTML = "allowHtml";
@@ -49,5 +52,7 @@
         "text/x-markdown");
     cfg.setString(SECTION_FORMATTER, PlainTextFormatter.NAME, KEY_MIME_TYPE,
         "text/plain");
+    cfg.setStringList(SECTION_FORMATTER, ZipFormatter.NAME, KEY_EXT,
+        Arrays.asList("jar", "war", "zip"));
   }
 }
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 42c0456..2f9ae4d 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
@@ -15,12 +15,9 @@
 package com.googlesource.gerrit.plugins.xdocs.formatter;
 
 import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_ALLOW_HTML;
-import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_CSS_THEME;
 import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_INCLUDE_TOC;
-import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_INHERIT_CSS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.annotations.PluginData;
@@ -35,17 +32,14 @@
 import org.asciidoctor.Options;
 import org.asciidoctor.OptionsBuilder;
 import org.asciidoctor.SafeMode;
-import org.eclipse.jgit.util.TemporaryBuffer;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.StringReader;
-import java.net.URL;
 import java.util.Properties;
 
 @Singleton
@@ -57,7 +51,6 @@
   private static final String ERUBY = "erb";
 
   private final File baseDir;
-  private final String defaultCss;
   private final Properties attributes;
   private final FormatterUtil util;
   private final Formatters formatters;
@@ -66,7 +59,6 @@
   public AsciidoctorFormatter(@PluginData File baseDir,
       FormatterUtil formatterUtil, Formatters formatters) throws IOException {
     this.baseDir = baseDir;
-    this.defaultCss = readCss();
     this.attributes = readAttributes();
     this.util = formatterUtil;
     this.formatters = formatters;
@@ -92,18 +84,7 @@
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ByteStreams.copy(input, out);
         String html = out.toString(UTF_8.name());
-        String cssTheme = projectCfg.getString(KEY_CSS_THEME);
-        String inheritedCss =
-            util.getInheritedCss(projectName, NAME, "asciidoctor", cssTheme);
-        String projectCss = util.getCss(projectName, "asciidoctor", cssTheme);
-        if (projectCfg.getBoolean(KEY_INHERIT_CSS, true)) {
-          return util.insertCss(html,
-              MoreObjects.firstNonNull(inheritedCss, defaultCss), projectCss);
-        } else {
-          return util.insertCss(html,
-              MoreObjects.firstNonNull(projectCss,
-                  MoreObjects.firstNonNull(inheritedCss, defaultCss)));
-        }
+        return util.applyCss(html, NAME, projectName);
       }
     } finally {
       if (!tmpFile.delete()) {
@@ -154,19 +135,6 @@
     return ab.get();
   }
 
-  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
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/FormatterUtil.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/FormatterUtil.java
index 2c49704..5d62a7e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/FormatterUtil.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/FormatterUtil.java
@@ -14,11 +14,13 @@
 
 package com.googlesource.gerrit.plugins.xdocs.formatter;
 
+import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_CSS_THEME;
 import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_INHERIT_CSS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.annotations.PluginData;
 import com.google.gerrit.extensions.annotations.PluginName;
@@ -40,20 +42,31 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
 
 @Singleton
 public class FormatterUtil {
+  private static final Logger log = LoggerFactory.getLogger(FormatterUtil.class);
+
   private final String pluginName;
   private final File baseDir;
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final Formatters formatters;
+  private final Map<String, String> defaultCss;
 
   @Inject
   FormatterUtil(@PluginName String pluginName,
@@ -66,6 +79,7 @@
     this.repoManager = repoManager;
     this.projectCache = projectCache;
     this.formatters = formatters;
+    this.defaultCss = new HashMap<>();
   }
 
   /**
@@ -191,6 +205,47 @@
     return null;
   }
 
+  public String applyCss(String html, String formatterName, String projectName)
+      throws IOException {
+    ConfigSection projectCfg =
+        formatters.getFormatterConfig(formatterName, projectName);
+    String cssName = formatterName.toLowerCase(Locale.US);
+    String cssTheme = projectCfg.getString(KEY_CSS_THEME);
+    String defaultCss = getDefaultCss(formatterName);
+    String inheritedCss =
+        getInheritedCss(projectName, formatterName, cssName, cssTheme);
+    String projectCss = getCss(projectName, cssName, cssTheme);
+    if (projectCfg.getBoolean(KEY_INHERIT_CSS, true)) {
+      return insertCss(html,
+          MoreObjects.firstNonNull(inheritedCss, defaultCss), projectCss);
+    } else {
+      return insertCss(html,
+          MoreObjects.firstNonNull(projectCss,
+              MoreObjects.firstNonNull(inheritedCss, defaultCss)));
+    }
+  }
+
+  private String getDefaultCss(String formatterName) throws IOException {
+    String css = defaultCss.get(formatterName) ;
+    if (css == null) {
+      URL url = FormatterUtil.class.getResource(
+          formatterName.toLowerCase(Locale.US) + ".css");
+      if (url != null) {
+        try (InputStream in = url.openStream();
+            TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024)) {
+          tmp.copy(in);
+          css = new String(tmp.toByteArray(), UTF_8);
+        }
+      } else {
+        log.info(String.format("No default CSS for formatter '%s' found.",
+            formatterName));
+        css = "";
+      }
+      defaultCss.put(formatterName, css);
+    }
+    return css;
+  }
+
   /**
    * Inserts the given CSS into the given HTML.
    *
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/HtmlBuilder.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/HtmlBuilder.java
new file mode 100644
index 0000000..c971df5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/HtmlBuilder.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2014 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.
+
+package com.googlesource.gerrit.plugins.xdocs.formatter;
+
+import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
+
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+public class HtmlBuilder {
+  private final DateFormat rfc2822DateFormatter;
+  private final StringBuilder html = new StringBuilder();
+
+  @Inject
+  HtmlBuilder(@GerritPersonIdent PersonIdent gerritIdent) {
+    rfc2822DateFormatter =
+        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+    rfc2822DateFormatter.setCalendar(Calendar.getInstance(
+        gerritIdent.getTimeZone(), Locale.US));
+  }
+
+  public HtmlBuilder startDocument() {
+    return open("html");
+  }
+
+  public HtmlBuilder endDocument() {
+    return close("html");
+  }
+
+  public HtmlBuilder openBody() {
+    return open("body");
+  }
+
+  public HtmlBuilder closeBody() {
+    return close("body");
+  }
+
+  public HtmlBuilder openHead() {
+    return open("head");
+  }
+
+  public HtmlBuilder closeHead() {
+    return close("head");
+  }
+
+  public HtmlBuilder openTable() {
+    return open("table");
+  }
+
+  public HtmlBuilder closeTable() {
+    return close("table");
+  }
+
+  public HtmlBuilder openRow() {
+    return open("tr");
+  }
+
+  public HtmlBuilder closeRow() {
+    return close("tr");
+  }
+
+  public HtmlBuilder appendCellHeader(String s) {
+    return open("th").append(s).close("th");
+  }
+
+  public HtmlBuilder appendCell(String s) {
+    return open("td").append(s).close("td");
+  }
+
+  public HtmlBuilder appendCell() {
+    return appendCell("");
+  }
+
+  public HtmlBuilder appendDateCell(long date) {
+    return open("td").appendDate(date).close("td");
+  }
+
+  public HtmlBuilder open(String tag) {
+    html.append("<").append(tag).append(">");
+    return this;
+  }
+
+  public HtmlBuilder close(String tag) {
+    html.append("</").append(tag).append(">");
+    return this;
+  }
+
+  public HtmlBuilder append(String s) {
+    html.append(escapeHtml(s));
+    return this;
+  }
+
+  public HtmlBuilder appendDate(long date) {
+    html.append(rfc2822DateFormatter.format(new Date(date)));
+    return this;
+  }
+
+  public String toString() {
+    return html.toString();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/ZipFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/ZipFormatter.java
new file mode 100644
index 0000000..fcb9bd8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/ZipFormatter.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2014 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.
+
+package com.googlesource.gerrit.plugins.xdocs.formatter;
+
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
+
+import org.apache.commons.io.FileUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class ZipFormatter implements StreamFormatter {
+  public static final String NAME = "ZIP";
+
+  private final FormatterUtil util;
+  private final HtmlBuilder html;
+
+  @Inject
+  ZipFormatter(
+      Formatters formatters,
+      FormatterUtil formatterUtil,
+      HtmlBuilder html) {
+    this.util = formatterUtil;
+    this.html = html;
+  }
+
+  @Override
+  public String format(String projectName, String revision, ConfigSection globalCfg,
+      InputStream raw) throws IOException {
+    html.startDocument()
+        .openHead()
+        .closeHead()
+        .openBody()
+        .openTable()
+        .appendCellHeader("name")
+        .appendCellHeader("size")
+        .appendCellHeader("last modified");
+    try (ZipInputStream zip = new ZipInputStream(raw)) {
+      for (ZipEntry entry; (entry = zip.getNextEntry()) != null;) {
+        html.openRow()
+            .appendCell(entry.getName());
+        if (!entry.isDirectory()) {
+          if (entry.getSize() != -1) {
+            html.appendCell(FileUtils.byteCountToDisplaySize(entry.getSize()));
+          } else {
+            html.appendCell("n/a");
+          }
+        } else {
+          html.appendCell();
+        }
+        html.appendDateCell(entry.getTime())
+            .closeRow();
+      }
+    }
+    html.closeTable()
+        .closeBody()
+        .endDocument();
+
+    return util.applyCss(html.toString(), NAME, projectName);
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index f7b2632..7477b6e 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -82,6 +82,13 @@
     <td><a href="../../../Documentation/licenses.html#Apache2_0">Apache2.0</a></td>
     <td><a href="http://commons.apache.org">http://commons.apache.org</a></td>
   </tr>
+  <tr>
+    <td><tt>ZipFormatter</tt></td>
+    <td><tt>ZIP</tt></td>
+    <td>Formatter for zip files.</td>
+    <td></td>
+    <td></td>
+  </tr>
 </table>
 
 <a id="htmlDiff">
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 26d773e..2b12b55 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -96,7 +96,7 @@
 	Overrides the [global configuration of `inheritCss`](#formatterInheritCss)
 	for this formatter.
 
-	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`
+	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`, `ZIP`
 
 	Default: `true` (CSS is inherited)
 
@@ -111,7 +111,7 @@
 	Overrides the [global configuration of `cssTheme`](#formatterCssTheme)
 	for this formatter.
 
-	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`
+	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`, `ZIP`
 
 	By default not set.
 
@@ -137,6 +137,7 @@
 
 * `ASCIIDOCTOR`: `@PLUGIN@/asciidoctor.css`
 * `MARKDOWN`: `@PLUGIN@/markdown.css`
+* `ZIP`: `@PLUGIN@/zip.css`
 
 If link:inheritCss[inheritCss] is set to true custom CSS files are
 inherited from parent projects.
@@ -158,6 +159,10 @@
     mimeType = text/x-markdown
   [formatter "PLAIN_TEXT"]
     mimeType = text/plain
+  [formatter "ZIP"]
+    ext = jar
+    ext = war
+    ext = zip
 ```
 
 Supported formatters:
@@ -165,6 +170,7 @@
 * `ASCIIDOCTOR`
 * `MARKDOWN`
 * `PLAIN_TEXT`
+* `ZIP`
 
 <a id="formatterExt">
 formatter.<formatter>.ext
@@ -243,7 +249,7 @@
 
 	Can be overridden on [project-level](#inheritCss).
 
-	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`
+	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`, `ZIP`
 
 	Default: `true` (CSS is inherited)
 
@@ -256,7 +262,7 @@
 
 	Can be overridden on [project-level](#cssTheme).
 
-	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`
+	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`, `ZIP`
 
 	By default not set.
 
@@ -295,3 +301,4 @@
 
 * `ASCIIDOCTOR`: `asciidoctor.css`
 * `MARKDOWN`: `markdown.css`
+* `ZIP`: `zip.css`