Support gzip compressing streaming HTML

startRenderStreamingHtml is used for slow to compute payloads, but may
also be useful for large payloads that can benefit from compression,
and don't want to prebuffer the entire response in memory.

Define a variant startRenderCompressedStreamingHtml that can output
a compressed page, even if this delays rendering in the browser.

Change-Id: I342b845ae36321b99f7e1913168ea50dfc92adbf
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
index 4ddfec1..874e593 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
@@ -225,7 +225,42 @@
       HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
       throws IOException {
     req.setAttribute(STREAMING_ATTRIBUTE, true);
-    return renderer.renderStreaming(res, templateName, startHtmlResponse(req, res, soyData));
+    return renderer.renderStreaming(res, false, templateName, startHtmlResponse(req, res, soyData));
+  }
+
+  /**
+   * Start a compressed, streaming HTML response with header and footer rendered by Soy.
+   *
+   * <p>A streaming template includes the special template {@code gitiles.streamingPlaceholder} at
+   * the point where data is to be streamed. The template before and after this placeholder is
+   * rendered using the provided data map.
+   *
+   * <p>The response will be gzip compressed (if the user agent supports it) to reduce bandwidth.
+   * This may delay rendering in the browser.
+   *
+   * @param req in-progress request.
+   * @param res in-progress response.
+   * @param templateName Soy template name; must be in one of the template files defined in {@link
+   *     Renderer}.
+   * @param soyData data for Soy.
+   * @return output stream to render to. The portion of the template before the placeholder is
+   *     already written and flushed; the portion after is written only on calling {@code close()}.
+   * @throws IOException an error occurred during rendering the header.
+   */
+  protected OutputStream startRenderCompressedStreamingHtml(
+      HttpServletRequest req,
+      HttpServletResponse res,
+      String templateName,
+      Map<String, ?> soyData)
+      throws IOException {
+    req.setAttribute(STREAMING_ATTRIBUTE, true);
+    boolean gzip = false;
+    if (acceptsGzipEncoding(req)) {
+      res.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
+      res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
+      gzip = true;
+    }
+    return renderer.renderStreaming(res, gzip, templateName, startHtmlResponse(req, res, soyData));
   }
 
   private Map<String, ?> startHtmlResponse(
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
index 9face08..12912c2 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
@@ -39,6 +39,7 @@
 import java.util.Map;
 import java.util.concurrent.ConcurrentMap;
 import java.util.function.Function;
+import java.util.zip.GZIPOutputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -165,6 +166,12 @@
 
   OutputStream renderStreaming(HttpServletResponse res, String templateName, Map<String, ?> soyData)
       throws IOException {
+    return renderStreaming(res, false, templateName, soyData);
+  }
+
+  OutputStream renderStreaming(
+      HttpServletResponse res, boolean gzip, String templateName, Map<String, ?> soyData)
+      throws IOException {
     String html = newRenderer(templateName).setData(soyData).render();
     int id = html.indexOf(PLACEHOLDER);
     checkArgument(id >= 0, "Template must contain %s", PLACEHOLDER);
@@ -172,7 +179,7 @@
     int lt = html.lastIndexOf('<', id);
     int gt = html.indexOf('>', id + PLACEHOLDER.length());
 
-    OutputStream out = res.getOutputStream();
+    OutputStream out = gzip ? new GZIPOutputStream(res.getOutputStream()) : res.getOutputStream();
     out.write(html.substring(0, lt).getBytes(UTF_8));
     out.flush();