Allow configuring date formatters with a fixed time zone

By default, the date format used by Gitiles includes the time zone, as
that matches git tools. However, some users/administrators may prefer
normalizing to a particular timezone so times are directly comparable
without doing timezone arithmetic.

Allow this arrangement by setting gitiles.fixedTimeZone to a valid
Java TimeZone ID. This causes all dates in the UI to be implicitly
converted to this timezone, and the now-redundant timezone offset to
be dropped from the output. (Server administrators may communicate to
their users out-of-band about what the fixed timezone is.)

Bug: 48
Change-Id: I11f369471121403d35dae56137fb7fb82a9b10eb
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DateFormatterBuilder.java b/gitiles-servlet/src/main/java/com/google/gitiles/DateFormatterBuilder.java
index 00d7372..e7c0549 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/DateFormatterBuilder.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/DateFormatterBuilder.java
@@ -14,6 +14,7 @@
 
 package com.google.gitiles;
 
+import com.google.common.base.Optional;
 import com.google.common.collect.Lists;
 
 import org.eclipse.jgit.lib.PersonIdent;
@@ -48,18 +49,22 @@
 
     public String format(PersonIdent ident) {
       DateFormat df = getDateFormat(format);
-      TimeZone tz = ident.getTimeZone();
-      if (tz == null) {
-        tz = SystemReader.getInstance().getTimeZone();
+      if (!fixedTz.isPresent()) {
+        TimeZone tz = ident.getTimeZone();
+        if (tz == null) {
+          tz = SystemReader.getInstance().getTimeZone();
+        }
+        df.setTimeZone(tz);
       }
-      df.setTimeZone(tz);
       return df.format(ident.getWhen());
     }
   }
 
+  private final Optional<TimeZone> fixedTz;
   private final ThreadLocal<List<DateFormat>> dfs;
 
-  DateFormatterBuilder() {
+  DateFormatterBuilder(Optional<TimeZone> fixedTz) {
+    this.fixedTz = fixedTz;
     this.dfs = new ThreadLocal<List<DateFormat>>();
   }
 
@@ -79,7 +84,12 @@
     }
     DateFormat df = result.get(format.ordinal());
     if (df == null) {
-      df = new SimpleDateFormat(format.fmt + " Z");
+      if (fixedTz.isPresent()) {
+        df = new SimpleDateFormat(format.fmt);
+        df.setTimeZone(fixedTz.get());
+      } else {
+        df = new SimpleDateFormat(format.fmt + " Z");
+      }
       result.set(format.ordinal(), df);
     }
     return result.get(format.ordinal());
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
index 34503c3..e4de1c0 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
@@ -21,6 +21,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Objects;
+import com.google.common.base.Optional;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.LinkedListMultimap;
@@ -50,6 +51,7 @@
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.TimeZone;
 import java.util.regex.Pattern;
 
 import javax.servlet.Filter;
@@ -423,7 +425,14 @@
 
   private void setDefaultDateFormatterBuilder() {
     if (dateFormatterBuilder == null) {
-      dateFormatterBuilder = new DateFormatterBuilder();
+      String tzStr = config.getString("gitiles", null, "fixedTimeZone");
+      Optional<TimeZone> tz;
+      if (tzStr == null) {
+        tz = Optional.absent();
+      } else {
+        tz = Optional.of(TimeZone.getTimeZone(tzStr));
+      }
+      dateFormatterBuilder = new DateFormatterBuilder(tz);
     }
   }
 
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/DateFormatterBuilderTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/DateFormatterBuilderTest.java
index c730fa9..915a9ef 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/DateFormatterBuilderTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/DateFormatterBuilderTest.java
@@ -16,6 +16,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.google.common.base.Optional;
 import static com.google.gitiles.DateFormatterBuilder.Format.DEFAULT;
 import static com.google.gitiles.DateFormatterBuilder.Format.ISO;
 
@@ -30,18 +31,52 @@
 public class DateFormatterBuilderTest {
   @Test
   public void defaultIncludingTimeZone() throws Exception {
-    DateFormatterBuilder dfb = new DateFormatterBuilder();
+    DateFormatterBuilder dfb =
+        new DateFormatterBuilder(Optional.<TimeZone> absent());
     PersonIdent ident = newIdent("Mon Jan 2 15:04:05 2006", "-0700");
     assertEquals("Mon Jan 02 15:04:05 2006 -0700", dfb.create(DEFAULT).format(ident));
   }
 
   @Test
+  public void defaultWithUtc() throws Exception {
+    DateFormatterBuilder dfb =
+        new DateFormatterBuilder(Optional.of(TimeZone.getTimeZone("UTC")));
+    PersonIdent ident = newIdent("Mon Jan 2 15:04:05 2006", "-0700");
+    assertEquals("Mon Jan 02 22:04:05 2006", dfb.create(DEFAULT).format(ident));
+  }
+
+  @Test
+  public void defaultWithOtherTimeZone() throws Exception {
+    DateFormatterBuilder dfb =
+        new DateFormatterBuilder(Optional.of(TimeZone.getTimeZone("GMT-0400")));
+    PersonIdent ident = newIdent("Mon Jan 2 15:04:05 2006", "-0700");
+    assertEquals("Mon Jan 02 18:04:05 2006", dfb.create(DEFAULT).format(ident));
+  }
+
+  @Test
   public void isoIncludingTimeZone() throws Exception {
-    DateFormatterBuilder dfb = new DateFormatterBuilder();
+    DateFormatterBuilder dfb =
+        new DateFormatterBuilder(Optional.<TimeZone> absent());
     PersonIdent ident = newIdent("Mon Jan 2 15:04:05 2006", "-0700");
     assertEquals("2006-01-02 15:04:05 -0700", dfb.create(ISO).format(ident));
   }
 
+  @Test
+  public void isoWithUtc() throws Exception {
+    DateFormatterBuilder dfb =
+        new DateFormatterBuilder(Optional.of(TimeZone.getTimeZone("UTC")));
+    PersonIdent ident = newIdent("Mon Jan 2 15:04:05 2006", "-0700");
+    assertEquals("2006-01-02 22:04:05", dfb.create(ISO).format(ident));
+  }
+
+  @Test
+  public void isoWithOtherTimeZone() throws Exception {
+    DateFormatterBuilder dfb =
+        new DateFormatterBuilder(Optional.of(TimeZone.getTimeZone("GMT-0400")));
+    PersonIdent ident = newIdent("Mon Jan 2 15:04:05 2006", "-0700");
+    assertEquals("2006-01-02 18:04:05", dfb.create(ISO).format(ident));
+  }
+
   private PersonIdent newIdent(String whenStr, String tzStr) throws ParseException {
     whenStr += " " + tzStr;
     Date when = GitDateParser.parse(whenStr, null);
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/RepositoryIndexServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/RepositoryIndexServletTest.java
index e5a821c..dff8f57 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/RepositoryIndexServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/RepositoryIndexServletTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.gitiles.TestGitilesUrls.URLS;
 
+import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
@@ -28,6 +29,7 @@
 
 import java.io.IOException;
 import java.util.Map;
+import java.util.TimeZone;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -43,7 +45,7 @@
     servlet = new RepositoryIndexServlet(
         new TestGitilesAccess(repo.getRepository()),
         new DefaultRenderer(),
-        new DateFormatterBuilder(),
+        new DateFormatterBuilder(Optional.<TimeZone> absent()),
         new TimeCache());
   }