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}