Move base64 response encoding into BinaryResult

Make it easy for any RestApiView to return binary data within a
base64 wrapper by marking a BinaryResult with base64() before it
is given to RestApiServlet. The servlet will process the base64
wrapping before gzip encoding, which may save on transfer cost.

Change-Id: I5ed7b8cb2b034b60654cc2574e627159b11a4f27
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 188011c..103874f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -61,6 +61,7 @@
   private String characterEncoding;
   private long contentLength = -1;
   private boolean gzip = true;
+  private boolean base64 = false;
 
   /** @return the MIME type of the result, for HTTP clients. */
   public String getContentType() {
@@ -110,6 +111,17 @@
     return this;
   }
 
+  /** @return true if the result must be base64 encoded. */
+  public boolean isBase64() {
+    return base64;
+  }
+
+  /** Wrap the binary data in base64 encoding. */
+  public BinaryResult base64() {
+    base64 = true;
+    return this;
+  }
+
   /**
    * Write or copy the result onto the specified output stream.
    *
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 1040da3..fa1e902 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Charsets.UTF_8;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static java.math.RoundingMode.CEILING;
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
 import static javax.servlet.http.HttpServletResponse.SC_CREATED;
@@ -26,6 +27,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
 
+import com.google.common.base.Charsets;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
@@ -38,6 +40,8 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
+import com.google.common.io.BaseEncoding;
+import com.google.common.math.IntMath;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.audit.HttpAuditEvent;
@@ -627,10 +631,28 @@
       HttpServletResponse res,
       BinaryResult bin) throws IOException {
     try {
-      res.setContentType(bin.getContentType());
       OutputStream dst = res.getOutputStream();
       try {
         long len = bin.getContentLength();
+        if (bin.isBase64() && 0 <= len && len <= (10 << 20)) {
+          final TemporaryBuffer.Heap buf = base64(bin);
+          len = buf.length();
+          base64(res, bin);
+          bin = new BinaryResult() {
+            @Override
+            public void writeTo(OutputStream os) throws IOException {
+              buf.writeTo(os, null);
+            }
+          }.setContentLength(len);
+        } else if (bin.isBase64()) {
+          len = -1;
+          base64(res, bin);
+          dst = BaseEncoding.base64().encodingStream(
+              new OutputStreamWriter(dst, Charsets.ISO_8859_1));
+        } else {
+          res.setContentType(bin.getContentType());
+        }
+
         boolean gzip = bin.canGzip() && acceptsGzip(req);
         if (gzip && 256 <= len && len <= (10 << 20)) {
           TemporaryBuffer.Heap buf = compress(bin);
@@ -656,6 +678,12 @@
     }
   }
 
+  private static void base64(HttpServletResponse res, BinaryResult bin) {
+    res.setContentType("text/plain; charset=ISO-8859-1");
+    res.setHeader("X-FYI-Content-Encoding", "base64");
+    res.setHeader("X-FYI-Content-Type", bin.getContentType());
+  }
+
   private static void replyUncompressed(HttpServletResponse res,
       OutputStream dst, BinaryResult bin, long len) throws IOException {
     if (0 <= len && len < Integer.MAX_VALUE) {
@@ -842,6 +870,18 @@
     return false;
   }
 
+  private static TemporaryBuffer.Heap base64(BinaryResult bin)
+      throws IOException {
+    int len = (int) bin.getContentLength();
+    int max = 4 * IntMath.divide(len, 3, CEILING);
+    TemporaryBuffer.Heap buf = heap(max);
+    OutputStream encoded = BaseEncoding.base64().encodingStream(
+        new OutputStreamWriter(buf, Charsets.ISO_8859_1));
+    bin.writeTo(encoded);
+    encoded.close();
+    return buf;
+  }
+
   private static TemporaryBuffer.Heap compress(BinaryResult bin)
       throws IOException {
     TemporaryBuffer.Heap buf = heap(20 << 20);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
index 569df6f..7c8ec34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -14,11 +14,9 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Charsets;
-import com.google.common.io.BaseEncoding;
+import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.StreamingResponse;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
@@ -32,7 +30,6 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.io.OutputStreamWriter;
 
 public class GetContent implements RestReadView<FileResource> {
   private final GitRepositoryManager repoManager;
@@ -43,7 +40,7 @@
   }
 
   @Override
-  public StreamingResponse apply(FileResource rsrc)
+  public BinaryResult apply(FileResource rsrc)
       throws ResourceNotFoundException, IOException {
     Project.NameKey project =
         rsrc.getRevision().getControl().getProject().getNameKey();
@@ -61,21 +58,13 @@
           throw new ResourceNotFoundException();
         }
         try {
-          final ObjectLoader loader = repo.open(tw.getObjectId(0));
-          return new StreamingResponse() {
+          final ObjectLoader object = repo.open(tw.getObjectId(0));
+          return new BinaryResult() {
             @Override
-            public String getContentType() {
-              return "text/plain;charset=UTF-8";
+            public void writeTo(OutputStream os) throws IOException {
+              object.copyTo(os);
             }
-
-            @Override
-            public void stream(OutputStream out) throws IOException {
-              OutputStream b64Out = BaseEncoding.base64().encodingStream(
-                  new OutputStreamWriter(out, Charsets.UTF_8));
-              loader.copyTo(b64Out);
-              b64Out.close();
-            }
-          };
+          }.setContentLength(object.getSize()).base64();
         } finally {
           tw.release();
         }