Add formatter to render images

This allows to compare old and new image in the preview diff screens.

The image formatter is only used if a diff is requested. If no diff is
requested the plain images are returned. This is important to render
images that are embedded in project documentation, e.g. in Markdown or
Asciidoc files.

Change-Id: I17edc14a42c74100d0588ccbbf601a20807e356a
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
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 3447806..4c91b4d 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.AsciidoctorFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.DocxFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.Formatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.ImageFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.MarkdownFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.PlainTextFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.ZipFormatter;
@@ -61,6 +62,9 @@
         .annotatedWith(Exports.named(MarkdownFormatter.NAME))
         .to(MarkdownFormatter.class);
     bind(Formatter.class)
+        .annotatedWith(Exports.named(ImageFormatter.NAME))
+        .to(ImageFormatter.class);
+    bind(Formatter.class)
         .annotatedWith(Exports.named(PlainTextFormatter.NAME))
         .to(PlainTextFormatter.class);
     bind(Formatter.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 41e3a67..05f0f9b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
@@ -16,6 +16,7 @@
 
 import com.googlesource.gerrit.plugins.xdocs.formatter.AsciidoctorFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.DocxFormatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.ImageFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.MarkdownFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.PlainTextFormatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.ZipFormatter;
@@ -51,6 +52,8 @@
     cfg.setString(SECTION_FORMATTER, AsciidoctorFormatter.NAME, KEY_EXT, "adoc");
     cfg.setStringList(SECTION_FORMATTER, DocxFormatter.NAME, KEY_EXT,
         Arrays.asList("docx", "pptx", "xlsx"));
+    cfg.setString(SECTION_FORMATTER, ImageFormatter.NAME, KEY_MIME_TYPE,
+        "image/*");
     cfg.setString(SECTION_FORMATTER, MarkdownFormatter.NAME, KEY_MIME_TYPE,
         "text/x-markdown");
     cfg.setString(SECTION_FORMATTER, PlainTextFormatter.NAME, KEY_MIME_TYPE,
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 eb44305..d34abc3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
@@ -181,23 +181,24 @@
       }
       ObjectId objectId = tw.getObjectId(0);
       ObjectLoader loader = repo.open(objectId);
-      return getHtml(formatter, repo, loader, key.getProject(), revId);
+      return getHtml(formatter, repo, loader, key.getProject(),
+          key.getResource(), revId);
     } finally {
       tw.release();
     }
   }
 
   private String getHtml(FormatterProvider formatter, Repository repo,
-      ObjectLoader loader, Project.NameKey project, ObjectId revId)
+      ObjectLoader loader, Project.NameKey project, String path, ObjectId revId)
       throws MethodNotAllowedException, IOException, GitAPIException,
       ResourceNotFoundException {
     Formatter f = formatter.get();
     if (f instanceof StringFormatter) {
       return getHtml(formatter.getName(), (StringFormatter) f, repo, loader,
-          project, revId);
+          project, path, revId);
     } else if (f instanceof StreamFormatter) {
       return getHtml(formatter.getName(), (StreamFormatter) f, repo, loader,
-          project, revId);
+          project, path, revId);
     } else {
       log.error(String.format("Unsupported formatter: %s", formatter.getName()));
       throw new ResourceNotFoundException();
@@ -206,8 +207,8 @@
 
   private String getHtml(String formatterName, StringFormatter f,
       Repository repo, ObjectLoader loader, Project.NameKey project,
-      ObjectId revId) throws MethodNotAllowedException, IOException,
-      GitAPIException {
+      String path, ObjectId revId) throws MethodNotAllowedException,
+      IOException, GitAPIException {
     byte[] bytes = loader.getBytes(Integer.MAX_VALUE);
     boolean isBinary = RawText.isBinary(bytes);
     if (formatterName.equals(Formatters.RAW_FORMATTER) && isBinary) {
@@ -218,15 +219,15 @@
     if (!isBinary) {
       raw = replaceMacros(repo, project, revId, abbrRevId, raw);
     }
-    return f.format(project.get(), abbrRevId,
+    return f.format(project.get(), path, abbrRevId, revId.getName(),
         getFormatterConfig(formatterName), raw);
   }
 
   private String getHtml(String formatterName, StreamFormatter f,
       Repository repo, ObjectLoader loader, Project.NameKey project,
-      ObjectId revId) throws IOException {
+      String path, ObjectId revId) throws IOException {
     try (InputStream raw = loader.openStream()) {
-      return ((StreamFormatter) f).format(project.get(),
+      return ((StreamFormatter) f).format(project.get(), path, revId.getName(),
           getAbbrRevId(repo, revId), getFormatterConfig(formatterName), raw);
     }
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
index d532fa2..d180c64 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
@@ -127,11 +127,22 @@
       mimeType = new MimeType(FileContentUtil.resolveContentType(
           state, key.file, FileMode.FILE, mimeType.toString()));
       FormatterProvider formatter = getFormatter(req, key);
+      if (isSafeImage(mimeType)) {
+        if (key.diffMode == DiffMode.NO_DIFF) {
+          // always return plain images when no diff is requested,
+          // use formatter on images only for diff mode
+          formatter = null;
+        }
+      } else {
+        if (formatter == null) {
+          throw new ResourceNotFoundException();
+        }
+      }
       if (formatter == null && !isSafeImage(mimeType)) {
         throw new ResourceNotFoundException();
-      }
+      } else if (isSafeImage(mimeType))
 
-      validateDiffMode(key, mimeType);
+      validateDiffMode(key, formatter, mimeType);
 
       ProjectControl projectControl = projectControlFactory.validateFor(key.project);
       String rev = getRevision(cfg,
@@ -233,10 +244,11 @@
     }
   }
 
-  private static void validateDiffMode(ResourceKey key, MimeType mimeType)
+  private static void validateDiffMode(ResourceKey key,
+      FormatterProvider formatter, MimeType mimeType)
       throws ResourceNotFoundException {
     if (key.diffMode != DiffMode.NO_DIFF
-        && (key.revisionB == null || isImage(mimeType))) {
+        && (key.revisionB == null || (formatter == null && isImage(mimeType)))) {
       throw new ResourceNotFoundException();
     }
   }
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 2f9ae4d..8bbe7a0 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
@@ -65,8 +65,8 @@
   }
 
   @Override
-  public String format(String projectName, String revision,
-      ConfigSection globalCfg, String raw) throws IOException {
+  public String format(String projectName, String path, String revision,
+      String abbrRev, ConfigSection globalCfg, String raw) throws IOException {
     if (!globalCfg.getBoolean(KEY_ALLOW_HTML, false)) {
       raw = suppressHtml(raw);
     }
@@ -79,7 +79,7 @@
         new File(baseDir, "tmp/asciidoctor-" + TimeUtil.nowTs().getNanos() + ".tmp");
     try {
       Asciidoctor.Factory.create(AsciidoctorFormatter.class.getClassLoader())
-          .render(raw, createOptions(projectCfg, revision, tmpFile));
+          .render(raw, createOptions(projectCfg, abbrRev, tmpFile));
       try (FileInputStream input = new FileInputStream(tmpFile)) {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ByteStreams.copy(input, out);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/DocxFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/DocxFormatter.java
index f9eb83a..863b3ed 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/DocxFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/DocxFormatter.java
@@ -41,8 +41,8 @@
   }
 
   @Override
-  public String format(String projectName, String revision, ConfigSection cfg,
-      InputStream raw) throws IOException {
+  public String format(String projectName, String path, String revision,
+      String abbrRev, ConfigSection cfg, InputStream raw) throws IOException {
     // Docx4J tries to load some resources dynamically. This fails if the Gerrit
     // core classloader is used since it doesn't see the resources that are
     // contained in the plugin jar. To make the resource loading work we
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/ImageFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/ImageFormatter.java
new file mode 100644
index 0000000..3bdc8af
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/ImageFormatter.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2015 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.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
+import com.googlesource.gerrit.plugins.xdocs.XDocServlet;
+
+import org.eclipse.jgit.lib.Constants;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@Singleton
+public class ImageFormatter implements StreamFormatter {
+  public static final String NAME = "IMAGE";
+
+  private final String pluginName;
+
+  @Inject
+  public ImageFormatter(@PluginName String pluginName) {
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  public String format(String projectName, String path, String revision,
+      String abbrRev, ConfigSection cfg, InputStream raw) throws IOException {
+    return "<img src=\"" + escapeHtml(getUrl(projectName, path, revision)) + "\"/>";
+  }
+
+  private String getUrl(String projectName, String path, String revision) {
+    StringBuilder url = new StringBuilder();
+    url.append("/plugins/");
+    url.append(pluginName);
+    url.append(XDocServlet.PATH_PREFIX);
+    url.append(Url.encode(projectName));
+    if (revision != null && !Constants.HEAD.equals(revision)) {
+      url.append("/rev/");
+      url.append(Url.encode(revision));
+    }
+    url.append("/");
+    url.append(Url.encode(path));
+    return url.toString();
+  }
+}
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 58a8966..df3a368 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
@@ -40,8 +40,8 @@
   }
 
   @Override
-  public String format(String projectName, String revision,
-      ConfigSection globalCfg, String raw) throws IOException {
+  public String format(String projectName, String path, String revision,
+      String abbrRev, ConfigSection globalCfg, String raw) throws IOException {
     ConfigSection projectCfg =
         formatters.getFormatterConfig(NAME, projectName);
     com.google.gerrit.server.documentation.MarkdownFormatter f =
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 6e36bba..8a3decd 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,8 +22,8 @@
   public final static String NAME = "PLAIN_TEXT";
 
   @Override
-  public String format(String projectName, String revision, ConfigSection cfg,
-      String raw) {
+  public String format(String projectName, String path, String revision,
+      String abbrRev, ConfigSection cfg, String raw) {
     return "<pre>" + escapeHtml(raw) + "</pre>";
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StreamFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StreamFormatter.java
index c435b23..4760248 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StreamFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StreamFormatter.java
@@ -25,12 +25,14 @@
    *
    * @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 path the file path
+   * @param revision the revision from which the file is loaded
+   * @param abbrRev the abbreviated revision from which the file is loaded
    * @param cfg the global configuration for this formatter
    * @param raw the raw stream
    * @return the content from the given stream formatted as html
    * @throws IOException thrown if the formatting fails
    */
-  public String format(String projectName, String revision, ConfigSection cfg,
-      InputStream raw) throws IOException;
+  public String format(String projectName, String path, String revision,
+      String abbrRev, ConfigSection cfg, InputStream raw) throws IOException;
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StringFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StringFormatter.java
index 952794c..cd528cc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StringFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StringFormatter.java
@@ -25,12 +25,14 @@
    *
    * @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 path the file path
+   * @param revision the revision from which the file is loaded
+   * @param abbrRev the abbreviated revision from which the file is loaded
    * @param cfg the global 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, String revision, ConfigSection cfg,
-      String raw) throws IOException;
+  public String format(String projectName, String path, String revision,
+      String abbrRev, ConfigSection cfg, String raw) throws IOException;
 }
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
index fcb9bd8..d1a843e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/ZipFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/ZipFormatter.java
@@ -41,8 +41,9 @@
   }
 
   @Override
-  public String format(String projectName, String revision, ConfigSection globalCfg,
-      InputStream raw) throws IOException {
+  public String format(String projectName, String path, String revision,
+      String abbrRev, ConfigSection globalCfg, InputStream raw)
+      throws IOException {
     html.startDocument()
         .openHead()
         .closeHead()
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 5039d46..7337f64 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -76,6 +76,13 @@
     </td>
   </tr>
   <tr>
+    <td><tt>ImageFormatter</tt></td>
+    <td><tt>IMAGE</tt></td>
+    <td>Formatter for images that were configured as safe.</td>
+    <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>MarkdownFormatter</tt></td>
     <td><tt>MARKDOWN</tt></td>
     <td>Formatter for documentation that is written in
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 5184c96..56f3bb9 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -162,6 +162,8 @@
     ext = docx
     ext = pptx
     ext = xlsx
+  [formatte "IMAGE"]
+    mimeType = image/*
   [formatter "MARKDOWN"]
     mimeType = text/x-markdown
   [formatter "PLAIN_TEXT"]
@@ -176,6 +178,7 @@
 
 * `ASCIIDOCTOR`
 * `DOCX`
+* `IMAGE`
 * `MARKDOWN`
 * `PLAIN_TEXT`
 * `ZIP`