Repository: incubator-brooklyn
Updated Branches:
  refs/heads/master e668d15e2 -> c9445a286


clean up time API and support date parsing

also a lot of help for time zones, because java seems bad with this,
and making time methods consistent and less ambiguous


Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/9fe4c7c4
Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/9fe4c7c4
Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/9fe4c7c4

Branch: refs/heads/master
Commit: 9fe4c7c413e6aeba6ec01cf2e10427d2829b8cb2
Parents: 1a28193
Author: Alex Heneveld <[email protected]>
Authored: Thu Jun 4 16:53:17 2015 +0100
Committer: Alex Heneveld <[email protected]>
Committed: Mon Jun 8 00:26:31 2015 +0100

----------------------------------------------------------------------
 docs/guide/ops/requirements.md                  |  17 +-
 .../main/java/brooklyn/util/text/Strings.java   |  26 +-
 .../main/java/brooklyn/util/time/Duration.java  |   7 +-
 .../main/java/brooklyn/util/time/Durations.java |   9 +-
 .../src/main/java/brooklyn/util/time/Time.java  | 488 +++++++++++++++++--
 .../test/java/brooklyn/util/time/TimeTest.java  | 157 ++++++
 6 files changed, 658 insertions(+), 46 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/9fe4c7c4/docs/guide/ops/requirements.md
----------------------------------------------------------------------
diff --git a/docs/guide/ops/requirements.md b/docs/guide/ops/requirements.md
index 52b96e4..7830bc9 100644
--- a/docs/guide/ops/requirements.md
+++ b/docs/guide/ops/requirements.md
@@ -28,7 +28,7 @@ Brooklyn has also been tested on Ubuntu 12.04 and OS X.
 
 ## Software Requirements
 
-Brooklyn requires Java (JRE or JDK), version 6 or version 7. The most recent 
version 7 is recommended.
+Brooklyn requires Java (JRE or JDK) minimum version 6. The latest release of 
version 7 or 8 is recommended.
 OpenJDK is recommended. Brooklyn has also been tested on IBM J9 and Oracle's 
JVM.
 
 * check your `iptables` or other firewall service, making sure that incoming 
connections on port 8443 is not blocked
@@ -54,6 +54,21 @@ For example, to open port 8443 in iptables, ues the command:
     /sbin/iptables -I INPUT -p TCP --dport 8443 -j ACCEPT
 
 
+### Locale
+
+Brooklyn expects a sensible set of locale information and time zones to be 
available;
+without this, some time-and-date handling may be surprising.
+
+Brooklyn parses and reports times according to the time zone set at the server.
+If Brooklyn is targetting geographically distributed users, 
+it is normally recommended that the server's time zone be set to UTC.
+
+
+### User Setup
+
+It is normally recommended that Brooklyn run as a non-root user with keys 
installed to `~/.ssh/id_rsa{,.pub}`. 
+
+
 ### Linux Kernel Entropy
 
 Check that the [linux kernel entropy](increase-entropy.html) is sufficient.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/9fe4c7c4/utils/common/src/main/java/brooklyn/util/text/Strings.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/text/Strings.java 
b/utils/common/src/main/java/brooklyn/util/text/Strings.java
index 05957c4..cf61fad 100644
--- a/utils/common/src/main/java/brooklyn/util/text/Strings.java
+++ b/utils/common/src/main/java/brooklyn/util/text/Strings.java
@@ -246,7 +246,7 @@ public class Strings {
         return Joiner.on("\n").join(Arrays.asList(lines));
     }
 
-    /** replaces all key->value entries from the replacement map in source 
(non-regex) */
+    /** NON-REGEX - replaces all key->value entries from the replacement map 
in source (non-regex) */
     @SuppressWarnings("rawtypes")
     public static String replaceAll(String source, Map replacements) {
         for (Object rr: replacements.entrySet()) {
@@ -256,11 +256,20 @@ public class Strings {
         return source;
     }
 
-    /** NON-REGEX replaceAll -
-     * replaces all instances in source, of the given pattern, with the given 
replacement
-     * (not  interpreting any arguments as regular expressions)
-     */
+    /** NON-REGEX replaceAll - see the better, explicitly named {@link 
#replaceAllNonRegex(String, String, String)}. */
     public static String replaceAll(String source, String pattern, String 
replacement) {
+        return replaceAllNonRegex(source, pattern, replacement);
+    }
+
+    /** 
+     * Replaces all instances in source, of the given pattern, with the given 
replacement
+     * (not interpreting any arguments as regular expressions).
+     * <p>
+     * This is actually the same as the very ambiguous {@link 
String#replace(CharSequence, CharSequence)},
+     * which does replace all, but not using regex like the similarly 
ambiguous {@link String#replaceAll(String, String)} as.
+     * Alternatively see {@link #replaceAllRegex(String, String, String)}.
+     */
+    public static String replaceAllNonRegex(String source, String pattern, 
String replacement) {
         if (source==null) return source;
         StringBuilder result = new StringBuilder(source.length());
         for (int i=0; i<source.length(); ) {
@@ -275,12 +284,7 @@ public class Strings {
         return result.toString();
     }
 
-    /** NON-REGEX replacement -- explicit method name for reabaility, doing 
same as Strings.replaceAll */
-    public static String replaceAllNonRegex(String source, String pattern, 
String replacement) {
-        return replaceAll(source, pattern, replacement);
-    }
-
-    /** REGEX replacement -- explicit method name for reabaility, doing same 
as String.replaceAll */
+    /** REGEX replacement -- explicit method name for reabaility, doing same 
as {@link String#replaceAll(String, String)}. */
     public static String replaceAllRegex(String source, String pattern, String 
replacement) {
         return source.replaceAll(pattern, replacement);
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/9fe4c7c4/utils/common/src/main/java/brooklyn/util/time/Duration.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/time/Duration.java 
b/utils/common/src/main/java/brooklyn/util/time/Duration.java
index 0f73ef8..3107bf4 100644
--- a/utils/common/src/main/java/brooklyn/util/time/Duration.java
+++ b/utils/common/src/main/java/brooklyn/util/time/Duration.java
@@ -133,7 +133,10 @@ public class Duration implements Comparable<Duration>, 
Serializable {
         return nanos;
     }
 
-    /** see {@link #of(Object)} and {@link Time#parseTimeString(String)} */
+    /** 
+     * See {@link Time#parseElapsedTime(String)}; 
+     * also accepts "forever" (and for those who prefer things exceedingly 
accurate, "practically_forever"). 
+     * Also see {@link #of(Object)}. */
     public static Duration parse(String textualDescription) {
         if (textualDescription==null) return null;
         if ("null".equalsIgnoreCase(textualDescription)) return null;
@@ -142,7 +145,7 @@ public class Duration implements Comparable<Duration>, 
Serializable {
         if ("practicallyforever".equalsIgnoreCase(textualDescription)) return 
Duration.PRACTICALLY_FOREVER;
         if ("practically_forever".equalsIgnoreCase(textualDescription)) return 
Duration.PRACTICALLY_FOREVER;
         
-        return new Duration(Time.parseTimeString(textualDescription), 
TimeUnit.MILLISECONDS);
+        return new Duration((long) 
Time.parseElapsedTimeAsDouble(textualDescription), TimeUnit.MILLISECONDS);
     }
 
     /** creates new {@link Duration} instance of the given length of time */

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/9fe4c7c4/utils/common/src/main/java/brooklyn/util/time/Durations.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/time/Durations.java 
b/utils/common/src/main/java/brooklyn/util/time/Durations.java
index 60e77f4..3553ff4 100644
--- a/utils/common/src/main/java/brooklyn/util/time/Durations.java
+++ b/utils/common/src/main/java/brooklyn/util/time/Durations.java
@@ -40,8 +40,15 @@ public class Durations {
         try {
             if (timeout==null || timeout.toMilliseconds()<0 || 
Duration.PRACTICALLY_FOREVER.equals(timeout))
                 return Maybe.of(t.get());
-            if (timeout.toMilliseconds()==0 && !t.isDone())
+            if (timeout.toMilliseconds()==0 && !t.isDone()) {
+                for (int i=0; i<10; i++) {
+                    // give it 10 nanoseconds to complete - heuristically this 
is often enough
+                    // (Thread.yield should do it, but often seems to have no 
effect, e.g. on Mac)
+                    Thread.yield();
+                    Thread.sleep(0, 1);
+                }
                 return Maybe.absent("Task "+t+" not completed when immediate 
completion requested");
+            }
             return Maybe.of(t.get(timeout.toMilliseconds(), 
TimeUnit.MILLISECONDS));
         } catch (TimeoutException e) {
             return Maybe.absent("Task "+t+" did not complete within "+timeout);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/9fe4c7c4/utils/common/src/main/java/brooklyn/util/time/Time.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/time/Time.java 
b/utils/common/src/main/java/brooklyn/util/time/Time.java
index 2059223..8fa52cb 100644
--- a/utils/common/src/main/java/brooklyn/util/time/Time.java
+++ b/utils/common/src/main/java/brooklyn/util/time/Time.java
@@ -20,26 +20,41 @@ package brooklyn.util.time;
 
 import java.text.DateFormat;
 import java.text.ParseException;
+import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.SimpleTimeZone;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.annotation.Nullable;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.collections.MutableList;
 import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.guava.Maybe;
 import brooklyn.util.text.Strings;
 
 import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Stopwatch;
 
 public class Time {
 
+    private static final Logger log = LoggerFactory.getLogger(Time.class);
+    
+    public static final String DATE_FORMAT_PREFERRED_W_TZ = "yyyy-MM-dd 
HH:mm:ss.SSS Z";
     public static final String DATE_FORMAT_PREFERRED = "yyyy-MM-dd 
HH:mm:ss.SSS";
     public static final String DATE_FORMAT_STAMP = "yyyyMMdd-HHmmssSSS";
     public static final String DATE_FORMAT_SIMPLE_STAMP = "yyyy-MM-dd-HHmm";
+    public static final String DATE_FORMAT_OF_DATE_TOSTRING = "EEE MMM dd 
HH:mm:ss zzz yyyy";
 
     public static final long MILLIS_IN_SECOND = 1000;
     public static final long MILLIS_IN_MINUTE = 60*MILLIS_IN_SECOND;
@@ -368,22 +383,36 @@ public class Time {
         return (result == 0) ? -1 : result;
     }
     
-    /** parses a string eg '5s' or '20m 22.123ms', returning the number of 
milliseconds it represents (rounded);
-     * -1 on blank or "never" or "off" or "false";
-     * number of millis if no units specified.
+    /** Convenience for {@link Duration#parse(String)}. */
+    public static Duration parseDuration(String timeString) {
+        return Duration.parse(timeString);
+    }
+    
+    /** 
+     * As {@link #parseElapsedTimeAsDouble(String)}. Consider using {@link 
#parseDuration(String)} for a more usable return type.
      * 
      * @throws NumberFormatException if cannot be parsed (or if null)
      */
+    public static long parseElapsedTime(String timeString) {
+        return (long) parseElapsedTimeAsDouble(timeString);
+    }
+    /** @deprecated since 0.7.0 see {@link #parseElapsedTime(String)} */ 
@Deprecated
     public static long parseTimeString(String timeString) {
-        return (long) parseTimeStringAsDouble(timeString);
+        return (long) parseElapsedTime(timeString);
     }
-
-    /** parses a string eg '5s' or '20m 22.123ms', returning the number of 
milliseconds it represents; -1 on blank or never or off or false;
-     * number of millis if no units specified.
+    /** @deprecated since 0.7.0 see {@link #parseElapsedTimeAsDouble(String)} 
*/ @Deprecated
+    public static double parseTimeStringAsDouble(String timeString) {
+        return parseElapsedTimeAsDouble(timeString);
+    }
+    
+    /** 
+     * Parses a string eg '5s' or '20m 22.123ms', returning the number of 
milliseconds it represents; 
+     * -1 on blank or never or off or false.
+     * Assumes unit is millisections if no unit is specified.
      * 
      * @throws NumberFormatException if cannot be parsed (or if null)
      */
-    public static double parseTimeStringAsDouble(String timeString) {
+    public static double parseElapsedTimeAsDouble(String timeString) {
         if (timeString==null)
             throw new NumberFormatException("GeneralHelper.parseTimeString 
cannot parse a null string");
         try {
@@ -434,7 +463,7 @@ public class Time {
             double d = Double.parseDouble(num);
             double dd = 0;
             if (timeString.length()>0) {
-                dd = parseTimeStringAsDouble(timeString);
+                dd = parseElapsedTimeAsDouble(timeString);
                 if (dd==-1) {
                     throw new NumberFormatException("cannot combine 
'"+timeString+"' with '"+num+" "+s+"'");
                 }
@@ -443,36 +472,433 @@ public class Time {
         }
     }
 
+    /** parses dates from string, accepting many formats including 
'YYYY-MM-DD', 'YYYY-MM-DD HH:mm:SS', or millis since UTC epoch */
+    public static Date parseDate(String input) {
+        if (input==null) return null;
+        return parseDateMaybe(input).get();
+    }
+    
+    /** as {@link #parseDate(String)} but returning a {@link Maybe} rather 
than throwing or returning null */
+    public static Maybe<Date> parseDateMaybe(String input) {
+        if (input==null) return Maybe.absent("value is null");
+        input = input.trim();
+        Maybe<Date> result;
+
+        result = parseDateUtc(input);
+        if (result.isPresent()) return result;
+
+        result = parseDateSimpleFlexibleFormatParser(input);
+        if (result.isPresent()) return result;
+        // return the error from this method
+        Maybe<Date> returnResult = result;
+
+//        // see natty method comments below
+//        Maybe<Date> result = parseDateNatty(input);
+//        if (result.isPresent()) return result;
+
+        result = parseDateFormat(input, new 
SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
+        if (result.isPresent()) return result;
+        result = parseDateDefaultParse(input);
+        if (result.isPresent()) return result;
+
+        return returnResult;
+    }
+
+    @SuppressWarnings("deprecation")
+    private static Maybe<Date> parseDateDefaultParse(String input) {
+        try {
+            long ms = Date.parse(input);
+            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 
1, 2).getTime()) {
+                // accept default date parse for this century and next
+                return Maybe.of(new Date(ms));
+            }
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+        }
+        return Maybe.absent();
+    }
+
+    private static Maybe<Date> parseDateUtc(String input) {
+        if (input.matches("\\d+")) {
+            Maybe<Date> result = Maybe.of(new Date(Long.parseLong(input)));
+            if (result.isPresent()) {
+                @SuppressWarnings("deprecation")
+                int year = result.get().getYear();
+                if (year >= 2000 && year < 2200) {
+                    // only applicable for dates in this century
+                    return result;
+                } else {
+                    return Maybe.absent("long is probably not millis since 
epoch UTC; millis as string is not in acceptable range");
+                }
+            }
+        }
+        return Maybe.absent("not long millis since epoch UTC");
+    }
+
+    private final static String DIGIT = "\\d";
+    private final static String LETTER = "\\p{L}";
+    private final static String COMMON_SEPARATORS = "-\\.";
+    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
+    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
+    private final static String TIME_ZONE_SEPARATOR = COMMON_SEPARATORS+":/ ,";
+    private final static String DATE_TIME_SEPARATOR = TIME_ZONE_SEPARATOR+"T'";
+
+    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
+            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
+            + anyChar(DATE_SEPARATOR)
+            + namedGroup("month", options(optionally(DIGIT)+DIGIT, 
anyChar(LETTER)+"+"))
+            + anyChar(DATE_SEPARATOR)
+            + namedGroup("day", optionally(DIGIT)+DIGIT);
+    private final static String DATE_WORDS_2 = 
+        namedGroup("month", anyChar(LETTER)+"+")
+        + anyChar(DATE_SEPARATOR)
+        + namedGroup("day", optionally(DIGIT)+DIGIT)
+        + ",?"+anyChar(DATE_SEPARATOR)+"+"
+        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
+    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing 
for MM-DD-YYYY oriented people, so require month named
+    private final static String DATE_WORDS_3 = 
+        namedGroup("day", optionally(DIGIT)+DIGIT)
+        + anyChar(DATE_SEPARATOR)
+        + namedGroup("month", anyChar(LETTER)+"+")
+        + ",?"+anyChar(DATE_SEPARATOR)+"+"
+        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
+
+    private final static String DATE_ONLY_NO_SEPARATORS = 
+            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
+            + namedGroup("month", DIGIT+DIGIT)
+            + namedGroup("day", DIGIT+DIGIT);
+
+    private final static String MERIDIAN = 
anyChar("aApP")+optionally(anyChar("mM"));
+    private final static String TIME_ONLY_WITH_INNER_SEPARATORS = 
+        namedGroup("hours", optionally(DIGIT)+DIGIT)+
+        optionally(
+            anyChar(TIME_SEPARATOR)+
+            namedGroup("mins", DIGIT+DIGIT)+
+            optionally(
+                anyChar(TIME_SEPARATOR)+
+                namedGroup("secs", DIGIT+DIGIT+optionally( 
optionally("\\.")+DIGIT+"+"))))+
+        optionally(" *" + namedGroup("meridian", 
notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
+    private final static String TIME_ONLY_NO_SEPARATORS = 
+        namedGroup("hours", DIGIT+DIGIT)+
+        namedGroup("mins", DIGIT+DIGIT)+
+        optionally(
+            namedGroup("secs", DIGIT+DIGIT+optionally( 
optionally("\\.")+DIGIT+"+")))+
+        namedGroup("meridian", "");
+
+    private final static String TZ_CODE = namedGroup("tzCode",
+        notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) // not AM or PM
+        + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
+    private final static String TIME_ZONE_SIGNED_OFFSET = namedGroup("tz", 
options(namedGroup("tzOffset", options("\\+", "-")+
+            DIGIT+optionally(DIGIT)+options("h", "H", "", 
optionally(":")+DIGIT+DIGIT)), 
+        optionally("\\+")+TZ_CODE));
+    private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = 
namedGroup("tz", options(namedGroup("tzOffset", options("\\+", "-", " ")+
+            options(optionally(DIGIT)+DIGIT+options("h", "H"), 
options("0"+DIGIT, "10", "11", "12")+optionally(":")+DIGIT+DIGIT)), 
+        TZ_CODE));
+
+    @SuppressWarnings("deprecation")
+    private static Maybe<Date> parseDateSimpleFlexibleFormatParser(String 
input) {
+        input = input.trim();
+
+        String[] DATE_PATTERNS = new String[] {
+            DATE_ONLY_WITH_INNER_SEPARATORS,
+            DATE_ONLY_NO_SEPARATORS,
+            DATE_WORDS_2,
+            DATE_WORDS_3,            
+        };
+        String[] TIME_PATTERNS = new String[] {
+            TIME_ONLY_WITH_INNER_SEPARATORS,
+            TIME_ONLY_NO_SEPARATORS            
+        };
+        String[] TZ_PATTERNS = new String[] {
+            // space then time zone with sign (+-) or code is preferred
+            " " + TIME_ZONE_SIGNED_OFFSET,
+            // then no TZ - but declare the named groups
+            namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", 
"")),
+            // then any separator then offset with sign
+            anyChar(TIME_ZONE_SEPARATOR)+"+" + TIME_ZONE_SIGNED_OFFSET,
+            
+            // try parsing with enforced separators before TZ first 
+            // (so e.g. in the case of DATE-0100, the -0100 is the time, not 
the timezone)
+            // then relax below (e.g. in the case of DATE-TIME+0100)
+            
+            // finally match DATE-TIME-1000 as time zone -1000
+            // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but 
converted to ' ' by web
+            // (but be stricter about the format, 2h or 0200 required, and 
hours <= 12 so as not to confuse with a year)
+            anyChar(TIME_ZONE_SEPARATOR)+"*" + 
TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
+        };
+        
+        List<String> basePatterns = MutableList.of();
+        
+        // patterns with date first
+        String[] DATE_PATTERNS_UNCLOSED = new String[] {
+            // separator before time *required* if date had separators
+            DATE_ONLY_WITH_INNER_SEPARATORS + "(,?"+(namedGroup("sep", 
anyChar(DATE_TIME_SEPARATOR)+"+")),
+            // separator before time optional if date did not have separators
+            DATE_ONLY_NO_SEPARATORS + "("+(namedGroup("sep", 
anyChar(DATE_TIME_SEPARATOR)+"*")),
+            // separator before time required if date had words, comma allowed 
but needs something else (e.g. space), 'T' for UTC not supported
+            DATE_WORDS_2 + 
"(,?"+anyChar(DATE_TIME_SEPARATOR)+"+"+namedGroup("sep",""),
+            DATE_WORDS_3 + 
"(,?"+anyChar(DATE_TIME_SEPARATOR)+"+"+namedGroup("sep",""),
+        };
+        for (String tzP: TZ_PATTERNS)
+            for (String dateP: DATE_PATTERNS_UNCLOSED)
+                for (String timeP: TIME_PATTERNS)
+                    basePatterns.add("^" + dateP + timeP+")?" + tzP + "$");
+        
+        // also allow time first, with TZ after, then before; separator needed 
but sep T for UTC not supported
+        for (String tzP: TZ_PATTERNS)
+            for (String dateP: DATE_PATTERNS)
+                for (String timeP: TIME_PATTERNS)
+                    basePatterns.add("^" + timeP + 
",?"+anyChar(DATE_TIME_SEPARATOR)+"+" +namedGroup("sep","") + dateP + tzP + 
"$");
+        // also allow time first, with TZ after, then before; separator needed 
but sep T for UTC not supported
+        for (String tzP: TZ_PATTERNS)
+            for (String dateP: DATE_PATTERNS)
+                for (String timeP: TIME_PATTERNS)
+                    basePatterns.add("^" + timeP + tzP + 
",?"+anyChar(DATE_TIME_SEPARATOR)+"+" +namedGroup("sep","") + dateP + "$");
+
+        Maybe<Matcher> mm = Maybe.absent();
+        for (String p: basePatterns) {
+            mm = match(p, input);
+            if (mm.isPresent()) break;
+        }
+        if (mm.isPresent()) {
+            Matcher m = mm.get();
+            Calendar result;
+
+            String tz = m.group("tz");
+            String sep = m.group("sep");
+            if (sep!=null && sep.trim().equals("T")) {
+                if (Strings.isNonBlank(tz)) {
+                    return Maybe.absent("Cannot use 'T' separator and specify 
time zone ("+tz+")");
+                }
+                tz = "UTC";
+            }
+            
+            int year = Integer.parseInt(m.group("year"));
+            int day = Integer.parseInt(m.group("day"));
+            
+            String monthS = m.group("month");
+            int month;
+            if (monthS.matches(DIGIT+"+")) {
+                month = Integer.parseInt(monthS)-1;
+            } else {
+                try {
+                    month = new 
SimpleDateFormat("yyyy-MMM-dd").parse("2015-"+monthS+"-15").getMonth();
+                } catch (ParseException e) {
+                    return Maybe.absent("Unknown date format '"+input+"': 
invalid month '"+monthS+"'; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
+                }
+            }
+            
+            if (Strings.isNonBlank(tz)) {
+                TimeZone tzz = null;
+                String tzCode = m.group("tzCode");
+                if (Strings.isNonBlank(tzCode)) {
+                    tz = tzCode;
+                }
+                if (tz.matches(DIGIT+"+")) {
+                    // stick a plus in front in case it was submitted by a web 
form and turned into a space
+                    tz = "+"+tz;
+                } else {
+                    tzz = getTimeZone(tz);
+                }
+                if (tzz==null) {
+                    Maybe<Matcher> tmm = match("^ 
?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")(h|H||"+optionally(":")+"(?<tzM>"+DIGIT+DIGIT+"))$",
 tz);
+                    if (tmm.isAbsent()) {
+                        return Maybe.absent("Unknown date format '"+input+"': 
invalid timezone '"+tz+"'; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
+                    }
+                    Matcher tm = tmm.get();
+                    String tzM = tm.group("tzM");
+                    int offset = (60*Integer.parseInt(tm.group("tzH")) + 
Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
+                    tzz = new SimpleTimeZone(offset*1000, tz);
+                }
+                tz = getTimeZoneOffsetString(tzz, year, month, day);
+                result = new GregorianCalendar(tzz);
+            } else {
+                result = new GregorianCalendar();
+            }
+            result.clear();
+            
+            result.set(Calendar.YEAR, year);
+            result.set(Calendar.MONTH, month);
+            result.set(Calendar.DAY_OF_MONTH, day);
+            if (m.group("hours")!=null) {
+                int hours = Integer.parseInt(m.group("hours"));
+                String meridian = m.group("meridian");
+                if (Strings.isNonBlank(meridian) && 
meridian.toLowerCase().startsWith("p")) {
+                    if (hours>12) {
+                        return Maybe.absent("Unknown date format '"+input+"': 
can't be "+hours+" PM; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
+                    }
+                    hours += 12;
+                }
+                result.set(Calendar.HOUR_OF_DAY, hours);
+                String minsS = m.group("mins");
+                if (Strings.isNonBlank(minsS)) {
+                    result.set(Calendar.MINUTE, Integer.parseInt(minsS));
+                }
+                String secsS = m.group("secs");
+                if (Strings.isBlank(secsS)) {
+                    // leave at zero
+                } else if (secsS.matches(DIGIT+DIGIT+"?")) {
+                    result.set(Calendar.SECOND, Integer.parseInt(secsS));
+                } else {
+                    double s = Double.parseDouble(secsS);
+                    if (s>=0 && s<=60) {
+                        // in double format, with correct period
+                    } else if (secsS.length()==5) {
+                        // allow ssSSS with no punctuation
+                        s = s/=1000;
+                    } else {
+                        return Maybe.absent("Unknown date format '"+input+"': 
invalid seconds '"+secsS+"'; try 'YYYY-MM-DD HH:mm:ss.SSS +0000'");
+                    }
+                    result.set(Calendar.SECOND, (int)s);
+                    result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000));
+                }
+            }
+            
+            return Maybe.of(result.getTime());
+        }
+        return Maybe.absent("Unknown date format '"+input+"'; try ISO-8601, or 
'yyyy-MM-dd' or 'yyyy-MM-dd HH:mm:ss +0000'");
+    }
+    
+    public static TimeZone getTimeZone(String code) {
+        if (code.indexOf('/')==-1) {
+            if ("Z".equals(code)) return getTimeZone("UTC");
+            
+            // get the time zone -- most short codes aren't accepted, so 
accept (and prefer) certain common codes
+            if ("EST".equals(code)) return getTimeZone("America/New_York");
+            if ("EDT".equals(code)) return getTimeZone("America/New_York");
+            if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
+            if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
+            if ("CST".equals(code)) return getTimeZone("America/Chicago");
+            if ("CDT".equals(code)) return getTimeZone("America/Chicago");
+            if ("MST".equals(code)) return getTimeZone("America/Denver");
+            if ("MDT".equals(code)) return getTimeZone("America/Denver");
+
+            if ("BST".equals(code)) return getTimeZone("Europe/London");  // 
otherwise BST is Bangladesh!
+            if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
+            // IST falls through to below, where it is treated as India (not 
Irish); IDT not recognised
+        }
+        
+        TimeZone tz = TimeZone.getTimeZone(code);
+        if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
+            // recognized
+            return tz;
+        }
+        // possibly unrecognized -- GMT returned if not known, bad TimeZone 
API!
+        String timeZones[] = TimeZone.getAvailableIDs();
+        for (String tzs: timeZones) {
+            if (tzs.equals(code)) return tz;
+        }
+        // definitely unrecognized
+        return null;
+    }
+    
+    /** convert a TimeZone e.g. Europe/London to an offset string as at the 
given day, e.g. +0100 or +0000 depending daylight savings,
+     * absent with nice error if zone unknown */
+    public static Maybe<String> getTimeZoneOffsetString(String tz, int year, 
int month, int day) {
+        TimeZone tzz = getTimeZone(tz);
+        if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
+        return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
+    }
+    
+    /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the 
{@link TimeZone} is already instantiated */
+    @SuppressWarnings("deprecation")
+    public static String getTimeZoneOffsetString(TimeZone tz, int year, int 
month, int day) {
+        int tzMins = tz.getOffset(new Date(year, month, 
day).getTime())/60/1000;
+        String tzStr = (tzMins<0 ? "-" : "+") + 
Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", 
"")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
+        return tzStr;
+    }
+
+    private static String namedGroup(String name, String pattern) {
+        return "(?<"+name+">"+pattern+")";
+    }
+    private static String anyChar(String charSet) {
+        return "["+charSet+"]";
+    }
+    private static String optionally(String pattern) {
+        return "("+pattern+")?";
+    }
+    private static String options(String ...patterns) {
+        return "("+Strings.join(patterns,"|")+")";
+    }
+    private static String notMatching(String pattern) {
+        return "(?!"+pattern+")";
+    }
+    
+    private static Maybe<Matcher> match(String pattern, String input) {
+        Matcher m = Pattern.compile(pattern).matcher(input);
+        if (m.find() && m.start()==0 && m.end()==input.length())
+            return Maybe.of(m);
+        return Maybe.absent();
+    }
+
+//    // TODO https://github.com/joestelmach/natty is cool, but it drags in 
ANTLR,
+//    // it doesn't support dashes between date and time, and 
+//    // it encourages relative time which would be awesome but only if we 
resolved it on read 
+//    private static Maybe<Date> parseDateNatty(String input) {
+//        Parser parser = new Parser();
+//        List<DateGroup> groups = parser.parse(input);
+//        if (groups.size()!=1) {
+//            return Maybe.absent("Invalid date format (multiple dates): 
"+input);
+//        }
+//        DateGroup group = Iterables.getOnlyElement(groups);
+//        if (!group.getText().equals(input)) {
+//            return Maybe.absent("Invalid date format (incomplete parse): 
"+input+" -> "+group.getText());
+//        }
+//        if (group.isRecurring()) {
+//            return Maybe.absent("Invalid date format (recurring): "+input);
+//        }
+//        
+//        List<Date> dates = group.getDates();
+//        if (dates.size()!=1) {
+//            return Maybe.absent("Invalid date format (multiple dates): 
"+input);
+//        }
+//        Date d = Iterables.getOnlyElement(dates);
+//        
+//        if (group.isTimeInferred()) {
+//            GregorianCalendar c1 = new GregorianCalendar();
+//            c1.setTime(d);
+//            c1 = new GregorianCalendar(c1.get(Calendar.YEAR), 
c1.get(Calendar.MONTH), c1.get(Calendar.DAY_OF_MONTH));
+//            d = c1.getTime();
+//        }
+//        
+//        return Maybe.of(d);
+//    }
+
     /**
      * Parses the given date, accepting either a UTC timestamp (i.e. a long), 
or a formatted date.
-     * @param dateString
-     * @param format
-     * @return
+     * <p>
+     * If no time zone supplied, this defaults to the TZ configured at the 
brooklyn server.
+     * 
+     * @deprecated since 0.7.0 use {@link #parseDate(String)} for general or 
{@link #parseDateFormat(String, DateFormat)} for a format,
+     * plus {@link #parseDateUtc(String)} if you want to accept UTC
      */
     public static Date parseDateString(String dateString, DateFormat format) {
-        if (dateString == null) 
-            throw new NumberFormatException("GeneralHelper.parseDateString 
cannot parse a null string");
-
-        try {
-            return format.parse(dateString);
-        } catch (ParseException e) {
-            // continue trying
-        }
+        Maybe<Date> r = parseDateFormat(dateString, format);
+        if (r.isPresent()) return r.get();
         
-        try {
-            // fix the usual ' ' => '+' nonsense when passing dates as query 
params
-            // note this is not URL unescaping, but converting to the date 
format that wants "+" in the middle and not spaces
-            String transformedDateString = dateString.replace(" ", "+");
-            return format.parse(transformedDateString);
-        } catch (ParseException e) {
-            // continue trying
+        r = parseDateUtc(dateString);
+        if (r.isPresent()) return r.get();
+
+        throw new IllegalArgumentException("Date " + dateString + " cannot be 
parsed as UTC millis or using format " + format);
+    }
+    public static Maybe<Date> parseDateFormat(String dateString, String 
format) {
+        return parseDateFormat(dateString, new SimpleDateFormat(format));
+    }
+    public static Maybe<Date> parseDateFormat(String dateString, DateFormat 
format) {
+        if (dateString == null) { 
+            throw new NumberFormatException("GeneralHelper.parseDateString 
cannot parse a null string");
         }
+        Preconditions.checkNotNull(format, "date format");
+        dateString = dateString.trim();
         
-        try {
-            return new Date(Long.parseLong(dateString.trim())); // try UTC 
millis number
-        } catch (NumberFormatException e) {
-            throw new IllegalArgumentException("Date " + dateString + " cannot 
be parsed as UTC millis or using format " + format);
+        ParsePosition p = new ParsePosition(0);
+        Date result = format.parse(dateString, p);
+        if (result!=null) {
+            // accept results even if the entire thing wasn't parsed, as 
enough was to match the requested format
+            return Maybe.of(result);
         }
+        if (log.isTraceEnabled()) log.trace("Could not parse date 
"+dateString+" using format "+format+": "+p);
+        return Maybe.absent();
     }
 
     /** removes milliseconds from the date object; needed if serializing to 
ISO-8601 format 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/9fe4c7c4/utils/common/src/test/java/brooklyn/util/time/TimeTest.java
----------------------------------------------------------------------
diff --git a/utils/common/src/test/java/brooklyn/util/time/TimeTest.java 
b/utils/common/src/test/java/brooklyn/util/time/TimeTest.java
index 0771b90..ae9f78e 100644
--- a/utils/common/src/test/java/brooklyn/util/time/TimeTest.java
+++ b/utils/common/src/test/java/brooklyn/util/time/TimeTest.java
@@ -19,6 +19,7 @@
 package brooklyn.util.time;
 
 import java.util.Date;
+import java.util.TimeZone;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -149,4 +150,160 @@ public class TimeTest {
         Assert.assertFalse(Time.hasElapsedSince(aFewSecondsAgo, 
Duration.TEN_SECONDS));
         Assert.assertTrue(Time.hasElapsedSince(-1, Duration.TEN_SECONDS));
     }
+
+    @Test(groups="Integration")  //because it depends on TZ's set up and 
parsing months
+    public void testTimeZones() {
+        // useful to debug, if new special time zones needed
+//        for (String id: TimeZone.getAvailableIDs()) {
+//            TimeZone tz = TimeZone.getTimeZone(id);
+//            System.out.println(id+": "+tz.getDisplayName()+" 
"+tz.getDisplayName(true, TimeZone.SHORT)+" "+tz);
+//        }
+        
+        Assert.assertEquals("+0100", 
Time.getTimeZoneOffsetString("Europe/London", 2015, 6, 4).get());
+        
+        Assert.assertEquals("-0500", Time.getTimeZoneOffsetString("EST", 2015, 
1, 4).get());
+        Assert.assertEquals("-0400", 
Time.getTimeZoneOffsetString("America/New_York", 2015, 6, 4).get());
+        Assert.assertEquals("-0500", 
Time.getTimeZoneOffsetString("America/New_York", 2015, 1, 4).get());
+        
+        // BST treated as British Time (not Bangladesh)
+        Assert.assertEquals("+0000", Time.getTimeZoneOffsetString("BST", 2015, 
1, 4).get());
+        Assert.assertEquals("+0100", Time.getTimeZoneOffsetString("BST", 2015, 
6, 4).get());
+        
+        // EST treated as EDT not fixed -0500
+        Assert.assertEquals("-0400", Time.getTimeZoneOffsetString("EST", 2015, 
6, 4).get());
+        
+        // these normally not recognized
+        Assert.assertEquals("-0400", Time.getTimeZoneOffsetString("EDT", 2015, 
6, 4).get());
+        Assert.assertEquals("-0500", Time.getTimeZoneOffsetString("EDT", 2015, 
1, 4).get());
+        
+        Assert.assertEquals("-0600", Time.getTimeZoneOffsetString("CST", 2015, 
1, 4).get());
+        Assert.assertEquals("-0700", Time.getTimeZoneOffsetString("MST", 2015, 
1, 4).get());
+        Assert.assertEquals("-0800", Time.getTimeZoneOffsetString("PST", 2015, 
1, 4).get());
+        
+        Assert.assertEquals("+0530", Time.getTimeZoneOffsetString("IST", 2015, 
1, 4).get());
+    }
+        
+    @Test
+    public void testParseDate() {
+        doTestParseDate(false);
+    }
+    
+    @Test(groups="Integration")  //because it depends on TZ's set up and 
parsing months
+    public void testParseDateIntegration() {
+        doTestParseDate(true);
+    }
+    
+    private void doTestParseDate(boolean integration) {
+        // explicit TZ inclusion 
+        assertDatesParseToEqual("2015.6.4.0000 +0100", "2015-06-04-0000 
+0100");
+        assertDatesParseToEqual("2015.6.4.0100 +0100", "2015-06-04-0000 
+0000");
+        assertDatesParseToEqual("2015.6.4.0100 -0100", "2015-06-04-0200 
+0000");
+        if (integration) assertDatesParseToEqual("20150604 BST", "2015-06-04 
+0100");
+        
+        // no TZ uses server default
+        assertDatesParseToEqual("2015.6.4.0000", "2015-06-04-0000 
"+Time.getTimeZoneOffsetString(TimeZone.getDefault(), 2015, 6, 4));
+        assertDatesParseToEqual("20150604", "2015-06-04-0000");
+
+        // parse TZ
+        if (integration) {
+            assertDatesParseToEqual("20150604 +BST", "2015-06-04 +0100");
+            assertDatesParseToEqual("20150604 - - - BST", "2015-06-04 +0100");
+            assertDatesParseToEqual("20150604--BST", "2015-06-04 +0100");
+            assertDatesParseToEqual("20150604-//-BST", "2015-06-04 +0100");
+        }
+        assertDatesParseToEqual("2015.6.4+0100", "2015-06-04-0000+0100");
+        assertDatesParseToEqual("20150604-+0100", "2015-06-04 +0100");
+        assertDatesParseToEqual("2015-6-4 +0100", "2015-06-04-0000 +0100");
+        assertDatesParseToEqual("2015-6-4 -0100", "2015-06-04-0000 -0100");
+        assertDatesParseToEqual("20150604-0000//-0100", "2015-06-04 -0100");
+        // ambiguous TZ/hours parse prefers hours
+        assertDatesParseToEqual("2015-6-4-0100", "2015-06-04-0100");
+        assertDatesParseToEqual("2015-6-4--0100", "2015-06-04-0100");
+
+        // formats without spaces
+        assertDatesParseToEqual("20150604080012", "2015-06-04-080012");
+        assertDatesParseToEqual("20150604080012 +1000", "2015-06-03-220012 
+0000");
+        assertDatesParseToEqual("20150604080012 -1000", "2015-06-04-180012 
+0000");
+        assertDatesParseToEqual("20150604080012.345 +1000", 
"2015-06-03-220012.345 +0000");
+        if (integration) {
+            assertDatesParseToEqual("20150604 BST", "2015-06-04 +0100");
+            assertDatesParseToEqual("20150604 Europe/London", "2015-06-04 
+0100");
+        }
+
+        // more misc tests
+        assertDatesParseToEqual("20150604 08:00:12.345", 
"2015-06-04-080012.345");
+        assertDatesParseToEqual("20150604-080012.345", 
"2015-06-04-080012.345");
+        assertDatesParseToEqual("2015-12-1", "2015-12-01-0000");
+        assertDatesParseToEqual("1066-12-1", "1066-12-01-0000");
+        Assert.assertEquals(Time.parseDate("2012-2-29").getTime(), 
Time.parseDate("2012-3-1").getTime() - 24*60*60*1000);
+        // perverse, but accepted for the time being:
+        Assert.assertEquals(Time.parseDate("2013-2-29").getTime(), 
Time.parseDate("2013-3-1").getTime());
+        
+        assertDatesParseToEqual("20150604T080012.345", 
"2015-06-04-080012.345+0000");
+        assertDatesParseToEqual("20150604080012.345Z", 
"2015-06-04-080012.345+0000");
+        
+        // accept am and pm
+        assertDatesParseToEqual("20150604 08:00:12.345a", 
"2015-06-04-080012.345");
+        assertDatesParseToEqual("20150604 08:00:12.345 PM", 
"2015-06-04-200012.345");
+        if (integration) assertDatesParseToEqual("20150604 08:00:12.345 am 
BST", "2015-06-04-080012.345 +0100");
+        
+        // accept month in words
+        if (integration) {
+            assertDatesParseToEqual("2015-Dec-1", "2015-12-01-0000");
+            assertDatesParseToEqual("2015 Dec 1", "2015-12-01-0000");
+            assertDatesParseToEqual("2015-DEC-1", "2015-12-01-0000");
+            assertDatesParseToEqual("2015 December 1", "2015-12-01-0000");
+            assertDatesParseToEqual("2015 December 1", "2015-12-01-0000");
+            assertDatesParseToEqual("2015-Mar-1", "2015-03-01-0000");
+            assertDatesParseToEqual("2015 Mar 1", "2015-03-01-0000");
+            assertDatesParseToEqual("2015-MAR-1", "2015-03-01-0000");
+            assertDatesParseToEqual("2015 March 1", "2015-03-01-0000");
+            assertDatesParseToEqual("2015 March 1", "2015-03-01-0000");
+        }
+        
+        // for month in words, allow selected other orders also
+        if (integration) {
+            assertDatesParseToEqual("1-Jun-2015", "2015-06-01-0000");
+            assertDatesParseToEqual("Jun 1, 2015", "2015-06-01-0000");
+            assertDatesParseToEqual("June 1, 2015, 4pm", "2015-06-01-1600");
+        }
+    
+        // also allow time first if separators are used
+        assertDatesParseToEqual("16:00, 2015-12-30", "2015-12-30-1600");
+        if (integration) {
+            assertDatesParseToEqual("4pm, Dec 1, 2015", "2015-12-01-1600");
+            assertDatesParseToEqual("16:00 30-Dec-2015", "2015-12-30-1600");
+        }
+        
+        // and if time comes first, TZ can be before or after date
+        assertDatesParseToEqual("4pm +0100, 2015-12-30", "2015-12-30-1600 
+0100");
+        assertDatesParseToEqual("4pm, 2015-12-30, +0100", "2015-12-30-1600 
+0100");
+        
+        // these ambiguous ones are accepted (maybe we'd rather not), 
+        // but they are interpreted sensibly, preferring the more sensible 
interpretation 
+        if (integration) assertDatesParseToEqual("16 Dec 1 2015", 
"2015-12-01-1600");
+        if (integration) assertDatesParseToEqual("16:30 1067 Dec 1 1066", 
"1067-12-01-1630 +1066");
+        assertDatesParseToEqual("1040 1045 12 1", "1045-12-01-1040");
+        assertDatesParseToEqual("1040 1045 12 1 +0h", "1045-12-01-1040Z");
+        if (integration) assertDatesParseToEqual("1045 Dec 1 1040", 
"1045-12-01-1040");
+        if (integration) assertDatesParseToEqual("10:40 Dec 1 1045", 
"1045-12-01-1040");
+        assertDatesParseToEqual("10.11-2020-12.01", "2020-12-01-1011");
+        if (integration) assertDatesParseToEqual("Oct.11 1045 12.01", 
"1045-10-11-1201");
+        if (integration) assertDatesParseToEqual("1040 1045 Dec 1 1030", 
"1045-12-01-1040 +1030");
+        assertDatesParseToEqual("1040 +02 2015 12 1", "2015-12-01-1040 +0200");
+        assertDatesParseToEqual("10:40:+02 2015 12 1", "2015-12-01-1040 
+0200");
+    }
+    
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testParseDateToString() {
+        // java.lang.AssertionError: for: Sun Jun 07 21:21:00 BST 2015 
(20150607-212100073) expected [Sun Jun 07 21:22:13 BST 2015] but found [Sun Jun 
07 21:21:00 BST 2015]
+        Date d = new Date();
+        d.setSeconds(0);
+        assertDatesParseToEqual(d.toString(), 
Time.makeDateStampString(d.getTime()));
+    }
+
+    private void assertDatesParseToEqual(String input, String expected) {
+        Assert.assertEquals(Time.parseDate(input).toString(), 
Time.parseDate(expected).toString(), "for: "+input+" ("+expected+")");
+    }
 }


Reply via email to