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}