Add template hashing support to Renderer

This allows servlets to embed the Soy template's hash
as part of an ETag for a resource rendered from that
template file.

Change-Id: I40e3e2163cf6dbfa04e5b5228448986652faaedf
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java
index 1de2f69..0a57b6b 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.hash.HashCode;
 import com.google.template.soy.SoyFileSet;
 import com.google.template.soy.tofu.SoyTofu;
 
@@ -37,10 +38,15 @@
   }
 
   @Override
+  public HashCode getTemplateHash(String soyFile) {
+    return computeTemplateHash(soyFile);
+  }
+
+  @Override
   protected SoyTofu getTofu() {
     SoyFileSet.Builder builder = SoyFileSet.builder()
         .setCompileTimeGlobals(globals);
-    for (URL template : templates) {
+    for (URL template : templates.values()) {
       try {
         checkState(new File(template.toURI()).exists(), "Missing Soy template %s", template);
       } catch (URISyntaxException e) {
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java
index 3893148..f3f244b 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java
@@ -48,7 +48,7 @@
         globals, staticPrefix, customTemplates, siteTitle);
     SoyFileSet.Builder builder = SoyFileSet.builder()
         .setCompileTimeGlobals(this.globals);
-    for (URL template : templates) {
+    for (URL template : templates.values()) {
       builder.add(template);
     }
     tofu = builder.build().compileToTofu();
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 d7814a8..c9e5e80 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
@@ -16,24 +16,31 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
+import com.google.common.collect.MapMaker;
 import com.google.common.collect.Maps;
+import com.google.common.hash.Funnels;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteStreams;
 import com.google.common.net.HttpHeaders;
 import com.google.template.soy.tofu.SoyTofu;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -88,14 +95,25 @@
     }
   }
 
-  protected ImmutableList<URL> templates;
+  protected ImmutableMap<String, URL> templates;
   protected ImmutableMap<String, String> globals;
+  private final ConcurrentMap<String, HashCode> hashes = new MapMaker()
+      .initialCapacity(SOY_FILENAMES.size())
+      .concurrencyLevel(1)
+      .makeMap();
 
   protected Renderer(Function<String, URL> resourceMapper, Map<String, String> globals,
       String staticPrefix, Iterable<URL> customTemplates, String siteTitle) {
     checkNotNull(staticPrefix, "staticPrefix");
-    Iterable<URL> allTemplates = FluentIterable.from(SOY_FILENAMES).transform(resourceMapper);
-    templates = ImmutableList.copyOf(Iterables.concat(allTemplates, customTemplates));
+
+    ImmutableMap.Builder<String, URL> b = ImmutableMap.builder();
+    for (String name : SOY_FILENAMES) {
+      b.put(name, resourceMapper.apply(name));
+    }
+    for (URL u : customTemplates) {
+      b.put(u.toString(), u);
+    }
+    templates = b.build();
 
     Map<String, String> allGlobals = Maps.newHashMap();
     for (Map.Entry<String, String> e : STATIC_URL_GLOBALS.entrySet()) {
@@ -106,6 +124,29 @@
     this.globals = ImmutableMap.copyOf(allGlobals);
   }
 
+  public HashCode getTemplateHash(String soyFile) {
+    HashCode h = hashes.get(soyFile);
+    if (h == null) {
+      h = computeTemplateHash(soyFile);
+      hashes.put(soyFile, h);
+    }
+    return h;
+  }
+
+  HashCode computeTemplateHash(String soyFile) {
+    URL u = templates.get(soyFile);
+    checkState(u != null, "Missing Soy template %s", soyFile);
+
+    Hasher h = Hashing.sha1().newHasher();
+    try (InputStream is = u.openStream();
+        OutputStream os = Funnels.asOutputStream(h)) {
+      ByteStreams.copy(is, os);
+    } catch (IOException e) {
+      throw new IllegalStateException("Missing Soy template " + soyFile, e);
+    }
+    return h.hash();
+  }
+
   void render(HttpServletRequest req, HttpServletResponse res,
       String templateName, Map<String, ?> soyData) throws IOException {
     res.setContentType("text/html");