Use JGit Config#getTimeUnit to parse durations

This simplifies the code and ensures more consistency with other
code using JGit.  It involves some changes to semantics:

- "0 sec" is a duration now!  Durations do not have to be nonzero.

- unfortunately, fractional units (like "5.2 sec") are not allowed any
  more.  You have to use integer expressions like "5200 ms".

- throws IllegalArgumentException instead of IllegalStateExpression
  for bad units

If disallowing fractional units proves too much of a hardship, this
can be fixed in jgit.

Change-Id: I2b0053ed3f8a119cd06cb3f5dbd6a4ea5c24ac5f
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ConfigUtil.java b/gitiles-servlet/src/main/java/com/google/gitiles/ConfigUtil.java
index 597a8e0..1915d73 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ConfigUtil.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ConfigUtil.java
@@ -14,6 +14,8 @@
 
 package com.google.gitiles;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
 import com.google.common.base.Optional;
 import com.google.common.base.Predicates;
 import com.google.common.cache.CacheBuilder;
@@ -28,6 +30,8 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import javax.annotation.Nullable;
+
 /** Utilities for working with {@link Config} objects. */
 public class ConfigUtil {
   /**
@@ -44,76 +48,10 @@
    * @return a standard duration representing the time read, or defaultValue.
    */
   public static Duration getDuration(
-      Config config, String section, String subsection, String name, Duration defaultValue) {
-    String valStr = config.getString(section, subsection, name);
-    if (valStr == null) {
-      return defaultValue;
-    }
-    valStr = valStr.trim();
-    if (valStr.isEmpty()) {
-      return defaultValue;
-    }
-    Duration val = parseDuration(valStr);
-    if (val == null) {
-      String key = section + (subsection != null ? "." + subsection : "") + "." + name;
-      throw new IllegalStateException("Not time unit: " + key + " = " + valStr);
-    }
-    return val;
-  }
-
-  /**
-   * Parse a duration value from a string.
-   * <p>
-   * Durations can be written with unit suffixes, for example {@code "1 s"} or
-   * {@code "5 days"}. If units are not specified, milliseconds are assumed.
-   *
-   * @param valStr the value to parse.
-   * @return a standard duration representing the time parsed, or null if not a
-   *     valid duration.
-   */
-  public static Duration parseDuration(String valStr) {
-    if (valStr == null) {
-      return null;
-    }
-    valStr = valStr.trim();
-    if (valStr.isEmpty()) {
-      return null;
-    }
-    Matcher m = matcher("^([1-9][0-9]*(?:\\.[0-9]*)?)\\s*(.*)$", valStr);
-    if (!m.matches()) {
-      return null;
-    }
-
-    String digits = m.group(1);
-    String unitName = m.group(2).trim();
-
-    TimeUnit unit;
-    if ("".equals(unitName)) {
-      unit = TimeUnit.MILLISECONDS;
-    } else if (anyOf(unitName, "ms", "millis", "millisecond", "milliseconds")) {
-      unit = TimeUnit.MILLISECONDS;
-    } else if (anyOf(unitName, "s", "sec", "second", "seconds")) {
-      unit = TimeUnit.SECONDS;
-    } else if (anyOf(unitName, "m", "min", "minute", "minutes")) {
-      unit = TimeUnit.MINUTES;
-    } else if (anyOf(unitName, "h", "hr", "hour", "hours")) {
-      unit = TimeUnit.HOURS;
-    } else if (anyOf(unitName, "d", "day", "days")) {
-      unit = TimeUnit.DAYS;
-    } else {
-      return null;
-    }
-
-    try {
-      if (digits.indexOf('.') == -1) {
-        long val = Long.parseLong(digits);
-        return new Duration(val * TimeUnit.MILLISECONDS.convert(1, unit));
-      }
-      double val = Double.parseDouble(digits);
-      return new Duration((long) (val * TimeUnit.MILLISECONDS.convert(1, unit)));
-    } catch (NumberFormatException nfe) {
-      return null;
-    }
+      Config config, String section, String subsection, String name,
+      @Nullable Duration defaultValue) {
+    long m = config.getTimeUnit(section, subsection, name, -1, MILLISECONDS);
+    return m == -1 ? defaultValue : Duration.millis(m);
   }
 
   /**
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/ConfigUtilTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/ConfigUtilTest.java
index e28b9fa..255d7b8 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/ConfigUtilTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/ConfigUtilTest.java
@@ -17,7 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gitiles.ConfigUtil.getDuration;
-import static com.google.gitiles.ConfigUtil.parseDuration;
+import static org.junit.Assert.fail;
 
 import org.eclipse.jgit.lib.Config;
 import org.joda.time.Duration;
@@ -39,8 +39,12 @@
     assertThat(t.getMillis()).isEqualTo(500);
 
     config.setString("core", "dht", "timeout", "5.2 sec");
-    t = getDuration(config, "core", "dht", "timeout", def);
-    assertThat(t.getMillis()).isEqualTo(5200);
+    try {
+      getDuration(config, "core", "dht", "timeout", def);
+      fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage("Invalid time unit value: core.dht.timeout=5.2 sec");
+    }
 
     config.setString("core", "dht", "timeout", "1 min");
     t = getDuration(config, "core", "dht", "timeout", def);
@@ -48,24 +52,37 @@
   }
 
   @Test
-  public void parseDurationReturnsDuration() throws Exception {
-    assertDoesNotParse(null);
-    assertDoesNotParse("");
-    assertDoesNotParse(" ");
-    assertParses(500, "500 ms");
-    assertParses(500, "500ms");
-    assertParses(500, " 500 ms ");
-    assertParses(5200, "5.2 sec");
-    assertParses(60000, "1 min");
+  public void getDurationCanReturnDefault() throws Exception {
+    Duration def = Duration.standardSeconds(1);
+    Config config = new Config();
+    Duration t;
+
+    t = getDuration(config, "core", null, "blank", def);
+    assertThat(t.getMillis()).isEqualTo(1000);
+
+    config.setString("core", null, "blank", "");
+    t = getDuration(config, "core", null, "blank", def);
+    assertThat(t.getMillis()).isEqualTo(1000);
+
+    config.setString("core", null, "blank", " ");
+    t = getDuration(config, "core", null, "blank", def);
+    assertThat(t.getMillis()).isEqualTo(1000);
   }
 
-  private static void assertDoesNotParse(String val) {
-    assertThat(parseDuration(val)).named(String.valueOf(val)).isNull();
-  }
+  @Test
+  public void nullAsDefault() throws Exception {
+    Config config = new Config();
+    Duration t;
 
-  private static void assertParses(long expectedMillis, String val) {
-    Duration actual = parseDuration(checkNotNull(val));
-    assertThat(actual).named(val).isNotNull();
-    assertThat(actual.getMillis()).named(val).isEqualTo(expectedMillis);
+    t = getDuration(config, "core", null, "blank", null);
+    assertThat(t).isNull();
+
+    config.setString("core", null, "blank", "");
+    t = getDuration(config, "core", null, "blank", null);
+    assertThat(t).isNull();
+
+    config.setString("core", null, "blank", " ");
+    t = getDuration(config, "core", null, "blank", null);
+    assertThat(t).isNull();
   }
 }