Compress most replies if client accepts gzip If the user agent announces it can accept gzip encoded replies, gzip before sending to save on transfer bandwidth. Streaming HTML responses are never gzip compressed. Individual writes may be small and become stuck in the gzip compression path, causing unexpected latency to view new results as they are output by the servlet. Change-Id: I4cde58793443c29960f7211378a27e0c29de7dac
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 6cc6e11..5c9fd36 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
@@ -23,6 +23,7 @@ import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; @@ -33,12 +34,14 @@ import org.joda.time.Instant; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.Type; import java.util.Map; +import java.util.zip.GZIPOutputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -197,7 +200,8 @@ */ protected void renderHtml(HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData) throws IOException { - renderer.render(res, templateName, startHtmlResponse(req, res, soyData)); + renderer.render(req, res, templateName, + startHtmlResponse(req, res, soyData)); } /** @@ -259,11 +263,10 @@ Type typeOfSrc) throws IOException { setApiHeaders(res, JSON); res.setStatus(SC_OK); - - Writer writer = newWriter(res); - newGsonBuilder(req).create().toJson(src, typeOfSrc, writer); - writer.write('\n'); - writer.close(); + try (Writer writer = newWriter(req, res)) { + newGsonBuilder(req).create().toJson(src, typeOfSrc, writer); + writer.write('\n'); + } } @SuppressWarnings("unused") // Used in subclasses. @@ -284,7 +287,7 @@ protected Writer startRenderText(HttpServletRequest req, HttpServletResponse res, String contentType) throws IOException { setApiHeaders(res, contentType); - return newWriter(res); + return newWriter(req, res); } /** @@ -322,9 +325,9 @@ res.setStatus(statusCode); setApiHeaders(res, TEXT); setCacheHeaders(res); - Writer out = newWriter(res); - out.write(message); - out.close(); + try (Writer out = newWriter(req, res)) { + out.write(message); + } } protected GitilesAccess getAccess(HttpServletRequest req) { @@ -364,7 +367,40 @@ return new OutputStreamWriter(os, res.getCharacterEncoding()); } - private Writer newWriter(HttpServletResponse res) throws IOException { - return newWriter(res.getOutputStream(), res); + private Writer newWriter(HttpServletRequest req, HttpServletResponse res) + throws IOException { + OutputStream out; + if (acceptsGzipEncoding(req)) { + res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + out = new GZIPOutputStream(res.getOutputStream()); + } else { + out = res.getOutputStream(); + } + return newWriter(out, res); + } + + protected static boolean acceptsGzipEncoding(HttpServletRequest req) { + String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING); + if (accepts == null) { + return false; + } + for (int b = 0; b < accepts.length();) { + int comma = accepts.indexOf(',', b); + int e = 0 <= comma ? comma : accepts.length(); + String term = accepts.substring(b, e).trim(); + if (term.equals(ENCODING_GZIP)) { + return true; + } + b = e + 1; + } + return false; + } + + protected static byte[] gzip(byte[] raw) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (GZIPOutputStream gz = new GZIPOutputStream(out)) { + gz.write(raw); + } + return out.toByteArray(); } }
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 676447b..d7814a8 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; +import com.google.common.net.HttpHeaders; import com.google.template.soy.tofu.SoyTofu; import java.io.File; @@ -34,6 +35,7 @@ import java.util.List; import java.util.Map; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** @@ -104,11 +106,15 @@ this.globals = ImmutableMap.copyOf(allGlobals); } - void render(HttpServletResponse res, String templateName, Map<String, ?> soyData) - throws IOException { + void render(HttpServletRequest req, HttpServletResponse res, + String templateName, Map<String, ?> soyData) throws IOException { res.setContentType("text/html"); res.setCharacterEncoding("UTF-8"); byte[] data = newRenderer(templateName).setData(soyData).render().getBytes(UTF_8); + if (BaseServlet.acceptsGzipEncoding(req)) { + res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + data = BaseServlet.gzip(data); + } res.setContentLength(data.length); res.getOutputStream().write(data); }