Cache *.nocache.js and don't cache the host page

Our host page is significantly smaller than the *.nocache.js produced
by the GWT compiler, and since its HTML it can also be transferred by
way of gzip deflate encoding.

We can improve browser loading performance (slightly) by allowing the
larger *.nocache.js to be cached at the edges, and marking the host
page as never cached.  To ensure the most current code is loaded we
embed a SHA-1 hash of the *.nocache.js content within its URL, thus
allowing the CacheControlFilter to mark it as cached for 1 year, and
ensuring that new versions of code will use a different URL, causing
the browser to load the new *.nocache.js version from the server.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/pom.xml b/pom.xml
index 5b06d08..9d72d53 100644
--- a/pom.xml
+++ b/pom.xml
@@ -446,7 +446,7 @@
     <dependency>
       <groupId>gwtexpui</groupId>
       <artifactId>gwtexpui</artifactId>
-      <version>1.0.2</version>
+      <version>1.0.3</version>
       <scope>compile</scope>
     </dependency>
 
diff --git a/src/main/java/com/google/gerrit/public/Gerrit.html b/src/main/java/com/google/gerrit/public/Gerrit.html
index 38c8e07..a0b4324 100644
--- a/src/main/java/com/google/gerrit/public/Gerrit.html
+++ b/src/main/java/com/google/gerrit/public/Gerrit.html
@@ -3,8 +3,8 @@
     <title>Gerrit Code Review</title>
     <meta name="gwt:property" content="locale=en_US" />
     <script id="gerrit_gerritconfig"></script>
-    <script type="text/javascript" language="javascript" src="com.google.gerrit.Gerrit.nocache.js"></script>
-    <style type="text/css" id="gerrit_sitecss"></style>
+    <script id="gerrit_module" type="text/javascript" language="javascript" src="com.google.gerrit.Gerrit.nocache.js"></script>
+    <style  id="gerrit_sitecss" type="text/css"></style>
     <link rel="icon" type="image/gif" href="favicon.ico" />
   </head>
   <body>
diff --git a/src/main/java/com/google/gerrit/server/HostPageServlet.java b/src/main/java/com/google/gerrit/server/HostPageServlet.java
index 461b332..20642a0f 100644
--- a/src/main/java/com/google/gerrit/server/HostPageServlet.java
+++ b/src/main/java/com/google/gerrit/server/HostPageServlet.java
@@ -19,13 +19,17 @@
 import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.gwtorm.client.OrmException;
 
+import org.spearce.jgit.lib.Constants;
+import org.spearce.jgit.lib.ObjectId;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.StringWriter;
+import java.security.MessageDigest;
 
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
@@ -35,9 +39,6 @@
 
 /** Sends the Gerrit host page to clients. */
 public class HostPageServlet extends HttpServlet {
-  static final long MAX_AGE = 5 * 60 * 1000L/* milliseconds */;
-  static final String CACHE_CTRL = "public, max-age=" + (MAX_AGE / 1000L);
-
   private String canonicalUrl;
   private byte[] hostPageRaw;
   private byte[] hostPageCompressed;
@@ -64,6 +65,7 @@
     if (hostDoc == null) {
       throw new ServletException("No " + hostPageName + " in CLASSPATH");
     }
+    fixModuleReference(hostDoc);
     injectJson(hostDoc, "gerrit_gerritconfig", Common.getGerritConfig());
     injectCssFile(hostDoc, "gerrit_sitecss", sitePath, "GerritSite.css");
     injectXmlFile(hostDoc, "gerrit_header", sitePath, "GerritSiteHeader.html");
@@ -148,6 +150,39 @@
     scriptNode.appendChild(hostDoc.createCDATASection(w.toString()));
   }
 
+  private void fixModuleReference(final Document hostDoc)
+      throws ServletException {
+    final Element scriptNode = HtmlDomUtil.find(hostDoc, "gerrit_module");
+    if (scriptNode == null) {
+      throw new ServletException("No gerrit_module to rewrite in host document");
+    }
+
+    final String src = scriptNode.getAttribute("src");
+    final InputStream in = getServletContext().getResourceAsStream("/" + src);
+    if (in == null) {
+      throw new ServletException("No " + src + " in webapp root");
+    }
+
+    final MessageDigest md = Constants.newMessageDigest();
+    try {
+      try {
+        final byte[] buf = new byte[1024];
+        int n;
+        while ((n = in.read(buf)) > 0) {
+          md.update(buf, 0, n);
+        }
+      } finally {
+        in.close();
+      }
+    } catch (IOException e) {
+      throw new ServletException("Failed reading " + src, e);
+    }
+
+    final String vstr = ObjectId.fromRaw(md.digest()).name();
+    scriptNode.removeAttribute("id");
+    scriptNode.setAttribute("src", src + "?content=" + vstr);
+  }
+
   @Override
   protected long getLastModified(final HttpServletRequest req) {
     return lastModified;
@@ -192,8 +227,9 @@
       tosend = hostPageRaw;
     }
 
-    rsp.setHeader("Cache-Control", CACHE_CTRL);
-    rsp.setDateHeader("Expires", System.currentTimeMillis() + MAX_AGE);
+    rsp.setHeader("Pragma", "no-cache");
+    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
+    rsp.setDateHeader("Expires", 0L);
     rsp.setDateHeader("Last-Modified", lastModified);
     rsp.setContentType("text/html");
     rsp.setCharacterEncoding(HtmlDomUtil.ENC);