Generalize DiffServlet's streaming renderer

Change-Id: I0734de1b48b5ee6fbe31fbefc88558a2d5d46f3c
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 846c3c1..80af135 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
@@ -33,6 +33,7 @@
 import org.joda.time.Instant;
 
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.lang.reflect.Type;
@@ -190,6 +191,34 @@
    */
   protected void renderHtml(HttpServletRequest req, HttpServletResponse res, String templateName,
       Map<String, ?> soyData) throws IOException {
+    renderer.render(res, templateName, startHtmlResponse(req, res, soyData));
+  }
+
+  /**
+   * Start a 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.
+   *
+   * @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 startRenderStreamingHtml(HttpServletRequest req,
+      HttpServletResponse res, String templateName, Map<String, ?> soyData) throws IOException {
+    return renderer.renderStreaming(res, templateName, startHtmlResponse(req, res, soyData));
+  }
+
+  private Map<String, ?> startHtmlResponse(HttpServletRequest req, HttpServletResponse res,
+      Map<String, ?> soyData) throws IOException {
     res.setContentType(FormatType.HTML.getMimeType());
     res.setCharacterEncoding(Charsets.UTF_8.name());
     setCacheHeaders(res);
@@ -208,7 +237,7 @@
     }
 
     res.setStatus(HttpServletResponse.SC_OK);
-    renderer.render(res, templateName, allData);
+    return allData;
   }
 
   /**
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java
index 5b091f2..c469962 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 
-import com.google.common.base.Charsets;
 import com.google.common.io.BaseEncoding;
 import com.google.gitiles.CommitData.Field;
 import com.google.gitiles.DateFormatter.Format;
@@ -50,7 +49,6 @@
 /** Serves an HTML page with all the diffs for a commit. */
 public class DiffServlet extends BaseServlet {
   private static final long serialVersionUID = 1L;
-  private static final String PLACEHOLDER = "id=\"DIFF_OUTPUT_BLOCK\"";
 
   private final Linkifier linkifier;
 
@@ -109,17 +107,10 @@
         data.put("breadcrumbs", view.getBreadcrumbs());
       }
 
-      String[] html = renderAndSplit(data);
-      res.setStatus(HttpServletResponse.SC_OK);
-      res.setContentType(FormatType.HTML.getMimeType());
-      res.setCharacterEncoding(Charsets.UTF_8.name());
       setCacheHeaders(res);
-
-      try (OutputStream out = res.getOutputStream()) {
-        out.write(html[0].getBytes(Charsets.UTF_8));
+      try (OutputStream out = startRenderStreamingHtml(req, res, "gitiles.diffDetail", data)) {
         DiffFormatter diff = new HtmlDiffFormatter(renderer, view, out);
         formatDiff(repo, oldTree, newTree, view.getPathPart(), diff);
-        out.write(html[1].getBytes(Charsets.UTF_8));
       }
     } finally {
       if (tw != null) {
@@ -180,20 +171,6 @@
     return (tw.getRawMode(0) & FileMode.TYPE_FILE) > 0;
   }
 
-  private String[] renderAndSplit(Map<String, Object> data) {
-    String html = renderer.newRenderer("gitiles.diffDetail")
-        .setData(data)
-        .render();
-    int id = html.indexOf(PLACEHOLDER);
-    if (id < 0) {
-      throw new IllegalStateException("Template must contain " + PLACEHOLDER);
-    }
-
-    int lt = html.lastIndexOf('<', id);
-    int gt = html.indexOf('>', id + PLACEHOLDER.length());
-    return new String[] {html.substring(0, lt), html.substring(gt + 1)};
-  }
-
   private static void formatDiff(Repository repo, AbstractTreeIterator oldTree,
       AbstractTreeIterator newTree, String path, DiffFormatter diff) throws IOException {
     try {
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 9ac3f03..8733d01 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
@@ -14,6 +14,7 @@
 
 package com.google.gitiles;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.base.Charsets;
@@ -27,6 +28,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.List;
@@ -36,6 +38,9 @@
 
 /** Renderer for Soy templates used by Gitiles. */
 public abstract class Renderer {
+  // Must match .streamingPlaceholder.
+  private static final String PLACEHOLDER = "id=\"STREAMED_OUTPUT_BLOCK\"";
+
   private static final List<String> SOY_FILENAMES = ImmutableList.of(
       "BlameDetail.soy",
       "Common.soy",
@@ -107,6 +112,49 @@
     res.getOutputStream().write(data);
   }
 
+  public OutputStream renderStreaming(HttpServletResponse res, String templateName,
+      Map<String, ?> soyData) throws IOException {
+    final String html = newRenderer(templateName)
+        .setData(soyData)
+        .render();
+    int id = html.indexOf(PLACEHOLDER);
+    checkArgument(id >= 0, "Template must contain %s", PLACEHOLDER);
+
+    int lt = html.lastIndexOf('<', id);
+    final int gt = html.indexOf('>', id + PLACEHOLDER.length());
+    final OutputStream out = res.getOutputStream();
+    out.write(html.substring(0, lt).getBytes(Charsets.UTF_8));
+    out.flush();
+
+    return new OutputStream() {
+      @Override
+      public void write(byte[] b) throws IOException {
+        out.write(b);
+      }
+
+      @Override
+      public void write(byte[] b, int off, int len) throws IOException {
+        out.write(b, off, len);
+      }
+
+      @Override
+      public void write(int b) throws IOException {
+        out.write(b);
+      }
+
+      @Override
+      public void flush() throws IOException {
+        out.flush();
+      }
+
+      @Override
+      public void close() throws IOException {
+        out.write(html.substring(gt + 1).getBytes(Charsets.UTF_8));
+        out.close();
+      }
+    };
+  }
+
   SoyTofu.Renderer newRenderer(String templateName) {
     return getTofu().newRenderer(templateName);
   }
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Common.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Common.soy
index 5b2950d..1ec6540 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Common.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Common.soy
@@ -102,3 +102,13 @@
 </body>
 </html>
 {/template}
+
+/**
+ * Placeholder for streaming rendering.
+ *
+ * Insert this in a template to use with
+ * Renderer#renderStreaming(HttpServletResponse, String).
+ */
+{template .streamingPlaceholder}
+<div id="STREAMED_OUTPUT_BLOCK" />
+{/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy
index faa5c01..7339f34 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy
@@ -30,7 +30,7 @@
 {if $commit}
   {call .commitDetail data="$commit" /}
 {/if}
-<div id="DIFF_OUTPUT_BLOCK" />
+{call .streamingPlaceholder /}
 
 {call .footer /}
 {/template}