You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/06/15 09:41:29 UTC

[05/10] incubator-brooklyn git commit: switch Time api around dates to be based on Calendar so we preserve time zone

switch Time api around dates to be based on Calendar so we preserve time zone


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

Branch: refs/heads/master
Commit: 51ba0aaf173259881476b88ce37456a5e0665d0a
Parents: c968211
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Tue Jun 9 11:17:22 2015 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Jun 10 18:38:35 2015 +0100

----------------------------------------------------------------------
 .../brooklyn/rest/resources/UsageResource.java  |  14 +--
 .../rest/resources/UsageResourceTest.java       | 124 +++++++++---------
 .../src/main/java/brooklyn/util/time/Time.java  | 126 ++++++++++++++-----
 .../test/java/brooklyn/util/time/TimeTest.java  |  34 ++++-
 4 files changed, 187 insertions(+), 111 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/51ba0aaf/usage/rest-server/src/main/java/brooklyn/rest/resources/UsageResource.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/brooklyn/rest/resources/UsageResource.java b/usage/rest-server/src/main/java/brooklyn/rest/resources/UsageResource.java
index 4251712..08c0668 100644
--- a/usage/rest-server/src/main/java/brooklyn/rest/resources/UsageResource.java
+++ b/usage/rest-server/src/main/java/brooklyn/rest/resources/UsageResource.java
@@ -21,7 +21,6 @@ package brooklyn.rest.resources;
 import static brooklyn.rest.util.WebResourceUtils.notFound;
 
 import java.net.URI;
-import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
 import java.util.Set;
@@ -41,6 +40,7 @@ import brooklyn.rest.domain.UsageStatistic;
 import brooklyn.rest.domain.UsageStatistics;
 import brooklyn.rest.transform.ApplicationTransformer;
 import brooklyn.util.exceptions.UserFacingException;
+import brooklyn.util.text.Strings;
 import brooklyn.util.time.Time;
 
 import com.google.common.base.Objects;
@@ -57,14 +57,6 @@ public class UsageResource extends AbstractBrooklynRestResource implements Usage
 
     private static final Set<Lifecycle> WORKING_LIFECYCLES = ImmutableSet.of(Lifecycle.RUNNING, Lifecycle.CREATED, Lifecycle.STARTING);
 
-    // SimpleDateFormat is not thread-safe, so give one to each thread
-    private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = new ThreadLocal<SimpleDateFormat>(){
-        @Override
-        protected SimpleDateFormat initialValue() {
-            return new SimpleDateFormat(DATE_FORMAT);
-        }
-    };
-    
     @Override
     public List<UsageStatistics> listApplicationsUsage(@Nullable String start, @Nullable String end) {
         log.debug("REST call to get application usage for all applications: dates {} -> {}", new Object[] {start, end});
@@ -256,10 +248,10 @@ public class UsageResource extends AbstractBrooklynRestResource implements Usage
     }
 
     private Date parseDate(String toParse, Date def) {
-        return (toParse == null) ? def : Time.parseDate(toParse, DATE_FORMATTER.get());
+        return Strings.isBlank(toParse) ? def : Time.parseDate(toParse);
     }
     
     private String format(Date date) {
-        return DATE_FORMATTER.get().format(date);
+        return Time.makeDateString(date, Time.DATE_FORMAT_ISO8601_NO_MILLIS, Time.TIME_ZONE_UTC);
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/51ba0aaf/usage/rest-server/src/test/java/brooklyn/rest/resources/UsageResourceTest.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/test/java/brooklyn/rest/resources/UsageResourceTest.java b/usage/rest-server/src/test/java/brooklyn/rest/resources/UsageResourceTest.java
index ad67c51..533b220 100644
--- a/usage/rest-server/src/test/java/brooklyn/rest/resources/UsageResourceTest.java
+++ b/usage/rest-server/src/test/java/brooklyn/rest/resources/UsageResourceTest.java
@@ -21,10 +21,9 @@ package brooklyn.rest.resources;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.util.Arrays;
-import java.util.Date;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Callable;
@@ -56,6 +55,7 @@ import brooklyn.rest.testing.BrooklynRestResourceTest;
 import brooklyn.rest.testing.mocks.RestMockSimpleEntity;
 import brooklyn.test.entity.TestApplication;
 import brooklyn.util.repeat.Repeater;
+import brooklyn.util.time.Time;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -70,8 +70,7 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
 
     private static final long TIMEOUT_MS = 10*1000;
     
-    private Date testStartTime;
-    private DateFormat format = new SimpleDateFormat(AbstractBrooklynRestResource.DATE_FORMAT);
+    private Calendar testStartTime;
     
     private final ApplicationSpec simpleSpec = ApplicationSpec.builder().name("simple-app").
             entities(ImmutableSet.of(new EntitySpec("simple-ent", RestMockSimpleEntity.class.getName()))).
@@ -82,20 +81,20 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
     public void setUpMethod() {
         ((ManagementContextInternal)getManagementContext()).getStorage().remove(LocalUsageManager.APPLICATION_USAGE_KEY);
         ((ManagementContextInternal)getManagementContext()).getStorage().remove(LocalUsageManager.LOCATION_USAGE_KEY);
-        testStartTime = new Date();
+        testStartTime = new GregorianCalendar();
     }
 
     @Test
     public void testListApplicationUsages() throws Exception {
         // Create an app
-        Date preStart = new Date();
+        Calendar preStart = new GregorianCalendar();
         String appId = createApp(simpleSpec);
-        Date postStart = new Date();
+        Calendar postStart = new GregorianCalendar();
         
         // We will retrieve usage from one millisecond after start; this guarantees to not be  
         // told about both STARTING+RUNNING, which could otherwise happen if they are in the 
         // same milliscond.
-        Date afterPostStart = new Date(postStart.getTime()+1);
+        Calendar afterPostStart = Time.newCalendarFromMillisSinceEpochUtc(postStart.getTime().getTime()+1);
         
         // Check that app's usage is returned
         ClientResponse response = client().resource("/v1/usage/applications").get(ClientResponse.class);
@@ -104,15 +103,15 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         UsageStatistics usage = Iterables.getOnlyElement(usages);
         assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
 
-        // check app ignored if endDate before app started
-        response = client().resource("/v1/usage/applications?start="+0+"&end="+(preStart.getTime()-1)).get(ClientResponse.class);
+        // check app ignored if endCalendar before app started
+        response = client().resource("/v1/usage/applications?start="+0+"&end="+(preStart.getTime().getTime()-1)).get(ClientResponse.class);
         assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
         usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
         assertTrue(Iterables.isEmpty(usages), "usages="+usages);
         
         // Wait, so that definitely asking about things that have happened (not things in the future, 
         // or events that are happening this exact same millisecond)
-        waitForFuture(afterPostStart.getTime());
+        waitForFuture(afterPostStart.getTime().getTime());
 
         // Check app start + end date truncated, even if running for longer (i.e. only tell us about this time window).
         // Note that start==end means we get a snapshot of the apps in use at that exact time.
@@ -121,7 +120,7 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         // The comparison does use the milliseconds passed in the REST call though.
         // The rounding down result should be the same as roundDown(afterPostStart), because that is the time-window
         // we asked for.
-        response = client().resource("/v1/usage/applications?start="+afterPostStart.getTime()+"&end="+afterPostStart.getTime()).get(ClientResponse.class);
+        response = client().resource("/v1/usage/applications?start="+afterPostStart.getTime().getTime()+"&end="+afterPostStart.getTime().getTime()).get(ClientResponse.class);
         assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
         usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
         usage = Iterables.getOnlyElement(usages);
@@ -129,9 +128,9 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         assertAppUsageTimesTruncated(usage, roundDown(afterPostStart), roundDown(afterPostStart));
 
         // Delete the app
-        Date preDelete = new Date();
+        Calendar preDelete = new GregorianCalendar();
         deleteApp(appId);
-        Date postDelete = new Date();
+        Calendar postDelete = new GregorianCalendar();
 
         // Deleted app still returned, if in time range
         response = client().resource("/v1/usage/applications").get(ClientResponse.class);
@@ -141,7 +140,7 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING, Status.DESTROYED), roundDown(preStart), postDelete);
         assertAppUsage(ImmutableList.copyOf(usage.getStatistics()).subList(2, 3), appId, ImmutableList.of(Status.DESTROYED), roundDown(preDelete), postDelete);
 
-        long afterPostDelete = postDelete.getTime()+1;
+        long afterPostDelete = postDelete.getTime().getTime()+1;
         waitForFuture(afterPostDelete);
         
         response = client().resource("/v1/usage/applications?start=" + afterPostDelete).get(ClientResponse.class);
@@ -159,9 +158,9 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
     @Test
     public void testGetApplicationUsage() throws Exception {
         // Create an app
-        Date preStart = new Date();
+        Calendar preStart = new GregorianCalendar();
         String appId = createApp(simpleSpec);
-        Date postStart = new Date();
+        Calendar postStart = new GregorianCalendar();
         
         // Normal request returns all
         ClientResponse response = client().resource("/v1/usage/applications/" + appId).get(ClientResponse.class);
@@ -198,9 +197,9 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         assertTrue(usage.getStatistics().isEmpty());
         
         // Delete the app
-        Date preDelete = new Date();
+        Calendar preDelete = new GregorianCalendar();
         deleteApp(appId);
-        Date postDelete = new Date();
+        Calendar postDelete = new GregorianCalendar();
 
         // Deleted app still returned, if in time range
         response = client().resource("/v1/usage/applications/" + appId).get(ClientResponse.class);
@@ -210,7 +209,7 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         assertAppUsage(ImmutableList.copyOf(usage.getStatistics()).subList(2, 3), appId, ImmutableList.of(Status.DESTROYED), roundDown(preDelete), postDelete);
 
         // Deleted app not returned if terminated before time range begins
-        long afterPostDelete = postDelete.getTime()+1;
+        long afterPostDelete = postDelete.getTime().getTime()+1;
         waitForFuture(afterPostDelete);
         response = client().resource("/v1/usage/applications/" + appId +"?start=" + afterPostDelete).get(ClientResponse.class);
         assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
@@ -243,9 +242,9 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         TestApplication app = ApplicationBuilder.newManagedApp(TestApplication.class, getManagementContext());
         SoftwareProcessEntityTest.MyService entity = app.createAndManageChild(brooklyn.entity.proxying.EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
         
-        Date preStart = new Date();
+        Calendar preStart = new GregorianCalendar();
         app.start(ImmutableList.of(location));
-        Date postStart = new Date();
+        Calendar postStart = new GregorianCalendar();
         Location machine = Iterables.getOnlyElement(entity.getLocations());
 
         // All machines
@@ -270,9 +269,9 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         SoftwareProcessEntityTest.MyService entity = app.createAndManageChild(brooklyn.entity.proxying.EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
         String appId = app.getId();
         
-        Date preStart = new Date();
+        Calendar preStart = new GregorianCalendar();
         app.start(ImmutableList.of(location));
-        Date postStart = new Date();
+        Calendar postStart = new GregorianCalendar();
         Location machine = Iterables.getOnlyElement(entity.getLocations());
 
         // For running machine
@@ -283,9 +282,9 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED), roundDown(preStart), postStart);
         
         // Stop the machine
-        Date preStop = new Date();
+        Calendar preStop = new GregorianCalendar();
         app.stop();
-        Date postStop = new Date();
+        Calendar postStop = new GregorianCalendar();
         
         // Deleted machine still returned, if in time range
         response = client().resource("/v1/usage/machines?application=" + appId).get(ClientResponse.class);
@@ -296,7 +295,7 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         assertMachineUsage(ImmutableList.copyOf(usage.getStatistics()).subList(1,2), appId, machine.getId(), ImmutableList.of(Status.DESTROYED), roundDown(preStop), postStop);
 
         // Terminated machines ignored if terminated since start-time
-        long futureTime = postStop.getTime()+1;
+        long futureTime = postStop.getTime().getTime()+1;
         waitForFuture(futureTime);
         response = client().resource("/v1/usage/applications?start=" + futureTime).get(ClientResponse.class);
         assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
@@ -320,18 +319,18 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         waitForTask(deletionTask.getId());
     }
     
-    private void assertDateOrders(Object context, Date... dates) {
-        if (dates.length <= 1) return;
+    private void assertCalendarOrders(Object context, Calendar... Calendars) {
+        if (Calendars.length <= 1) return;
         
-        long[] times = new long[dates.length];
+        long[] times = new long[Calendars.length];
         for (int i = 0; i < times.length; i++) {
-            times[i] = millisSinceStart(dates[i]);
+            times[i] = millisSinceStart(Calendars[i]);
         }
-        String err = "context="+context+"; dates="+Arrays.toString(dates) + "; datesSanitized="+Arrays.toString(times);
+        String err = "context="+context+"; Calendars="+Arrays.toString(Calendars) + "; CalendarsSanitized="+Arrays.toString(times);
         
-        Date date = dates[0];
-        for (int i = 1; i < dates.length; i++) {
-            assertTrue(date.getTime() <= dates[i].getTime(), err);
+        Calendar Calendar = Calendars[0];
+        for (int i = 1; i < Calendars.length; i++) {
+            assertTrue(Calendar.getTime().getTime() <= Calendars[i].getTime().getTime(), err);
         }
     }
     
@@ -353,56 +352,56 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         assertTrue(success, "task "+taskId+" not finished");
     }
 
-    private long millisSinceStart(Date time) {
-        return time.getTime() - testStartTime.getTime();
+    private long millisSinceStart(Calendar time) {
+        return time.getTime().getTime() - testStartTime.getTime().getTime();
     }
     
-    private Date roundDown(Date date) {
-        long time = date.getTime();
+    private Calendar roundDown(Calendar calendar) {
+        long time = calendar.getTime().getTime();
         long timeDown = ((long)(time / 1000)) * 1000;
-        return new Date(timeDown);
+        return Time.newCalendarFromMillisSinceEpochUtc(timeDown);
     }
     
     @SuppressWarnings("unused")
-    private Date roundUp(Date date) {
-        long time = date.getTime();
+    private Calendar roundUp(Calendar calendar) {
+        long time = calendar.getTime().getTime();
         long timeDown = ((long)(time / 1000)) * 1000;
         long timeUp = (time == timeDown) ? time : timeDown + 1000;
-        return new Date(timeUp);
+        return Time.newCalendarFromMillisSinceEpochUtc(timeUp);
     }
 
-    private void assertMachineUsage(UsageStatistics usage, String appId, String machineId, List<Status> states, Date pre, Date post) throws Exception {
+    private void assertMachineUsage(UsageStatistics usage, String appId, String machineId, List<Status> states, Calendar pre, Calendar post) throws Exception {
         assertUsage(usage.getStatistics(), appId, machineId, states, pre, post, false);
     }
     
-    private void assertMachineUsage(Iterable<UsageStatistic> usages, String appId, String machineId, List<Status> states, Date pre, Date post) throws Exception {
+    private void assertMachineUsage(Iterable<UsageStatistic> usages, String appId, String machineId, List<Status> states, Calendar pre, Calendar post) throws Exception {
         assertUsage(usages, appId, machineId, states, pre, post, false);
     }
     
-    private void assertAppUsage(UsageStatistics usage, String appId, List<Status> states, Date pre, Date post) throws Exception {
+    private void assertAppUsage(UsageStatistics usage, String appId, List<Status> states, Calendar pre, Calendar post) throws Exception {
         assertUsage(usage.getStatistics(), appId, appId, states, pre, post, false);
     }
     
-    private void assertAppUsage(Iterable<UsageStatistic> usages, String appId, List<Status> states, Date pre, Date post) throws Exception {
+    private void assertAppUsage(Iterable<UsageStatistic> usages, String appId, List<Status> states, Calendar pre, Calendar post) throws Exception {
         assertUsage(usages, appId, appId, states, pre, post, false);
     }
 
-    private void assertUsage(Iterable<UsageStatistic> usages, String appId, String id, List<Status> states, Date pre, Date post, boolean allowGaps) throws Exception {
+    private void assertUsage(Iterable<UsageStatistic> usages, String appId, String id, List<Status> states, Calendar pre, Calendar post, boolean allowGaps) throws Exception {
         String errMsg = "usages="+usages;
-        Date now = new Date();
-        Date lowerBound = pre;
-        Date strictStart = null;
+        Calendar now = new GregorianCalendar();
+        Calendar lowerBound = pre;
+        Calendar strictStart = null;
         
         assertEquals(Iterables.size(usages), states.size(), errMsg);
         for (int i = 0; i < Iterables.size(usages); i++) {
             UsageStatistic usage = Iterables.get(usages, i);
-            Date usageStart = format.parse(usage.getStart());
-            Date usageEnd = format.parse(usage.getEnd());
+            Calendar usageStart = Time.parseCalendar(usage.getStart());
+            Calendar usageEnd = Time.parseCalendar(usage.getEnd());
             assertEquals(usage.getId(), id, errMsg);
             assertEquals(usage.getApplicationId(), appId, errMsg);
             assertEquals(usage.getStatus(), states.get(i), errMsg);
-            assertDateOrders(usages, lowerBound, usageStart, post);
-            assertDateOrders(usages, usageEnd, now);
+            assertCalendarOrders(usages, lowerBound, usageStart, post);
+            assertCalendarOrders(usages, usageEnd, now);
             if (strictStart != null) {
                 assertEquals(usageStart, strictStart, errMsg);
             }
@@ -413,12 +412,13 @@ public class UsageResourceTest extends BrooklynRestResourceTest {
         }
     }
 
-    private void assertAppUsageTimesTruncated(UsageStatistics usages, Date strictStart, Date strictEnd) throws Exception {
-        String errMsg = "usages="+usages+"; strictStart="+strictStart+"; strictEnd="+strictEnd;
-        Date usageStart = format.parse(Iterables.getFirst(usages.getStatistics(), null).getStart());
-        Date usageEnd = format.parse(Iterables.getLast(usages.getStatistics()).getStart());
-        assertEquals(usageStart, strictStart, errMsg);
-        assertEquals(usageEnd, strictEnd, errMsg);
+    private void assertAppUsageTimesTruncated(UsageStatistics usages, Calendar strictStart, Calendar strictEnd) throws Exception {
+        String errMsg = "strictStart="+Time.makeDateString(strictStart)+"; strictEnd="+Time.makeDateString(strictEnd)+";usages="+usages;
+        Calendar usageStart = Time.parseCalendar(Iterables.getFirst(usages.getStatistics(), null).getStart());
+        Calendar usageEnd = Time.parseCalendar(Iterables.getLast(usages.getStatistics()).getStart());
+        // time zones might be different - so must convert to date
+        assertEquals(usageStart.getTime(), strictStart.getTime(), "usageStart="+Time.makeDateString(usageStart)+";"+errMsg);
+        assertEquals(usageEnd.getTime(), strictEnd.getTime(), errMsg);
     }
     
     public static class DynamicLocalhostMachineProvisioningLocation extends LocalhostMachineProvisioningLocation {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/51ba0aaf/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 1c302d6..9e29ae2 100644
--- a/utils/common/src/main/java/brooklyn/util/time/Time.java
+++ b/utils/common/src/main/java/brooklyn/util/time/Time.java
@@ -55,6 +55,8 @@ public class Time {
     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 String DATE_FORMAT_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+    public static final String DATE_FORMAT_ISO8601_NO_MILLIS = "yyyy-MM-dd'T'HH:mm:ssZ";
 
     public static final long MILLIS_IN_SECOND = 1000;
     public static final long MILLIS_IN_MINUTE = 60*MILLIS_IN_SECOND;
@@ -62,15 +64,41 @@ public class Time {
     public static final long MILLIS_IN_DAY = 24*MILLIS_IN_HOUR;
     public static final long MILLIS_IN_YEAR = 365*MILLIS_IN_DAY;
     
-    /** returns the current time in {@value #DATE_FORMAT_PREFERRED} format,
-     * numeric big-endian but otherwise optimized for people to read, with spaces and colons and dots */
+    /** GMT/UTC/Z time zone constant */
+    public static final TimeZone TIME_ZONE_UTC = TimeZone.getTimeZone("");
+    
+    /** as {@link #makeDateString(Date)} for current date/time */
     public static String makeDateString() {
         return makeDateString(System.currentTimeMillis());
     }
 
-    /** returns the time in {@value #DATE_FORMAT_PREFERRED} format, given a long (e.g. returned by System.currentTimeMillis) */
+    /** as {@link #makeDateString(Date)} for long millis since UTC epock */
     public static String makeDateString(long date) {
-        return new SimpleDateFormat(DATE_FORMAT_PREFERRED).format(new Date(date));
+        return makeDateString(new Date(date), DATE_FORMAT_PREFERRED);
+    }
+    /** returns the time in {@value #DATE_FORMAT_PREFERRED} format for the given date;
+     * this format is numeric big-endian but otherwise optimized for people to read, with spaces and colons and dots;
+     * time is local to the server and time zone is <i>not</i> included */
+    public static String makeDateString(Date date) {
+        return makeDateString(date, DATE_FORMAT_PREFERRED);
+    }
+    /** as {@link #makeDateString(Date, String, TimeZone)} for the local time zone */
+    public static String makeDateString(Date date, String format) {
+        return makeDateString(date, format, null);
+    }
+    /** as {@link #makeDateString(Date, String, TimeZone)} for the given time zone; consider {@link TimeZone#GMT} */
+    public static String makeDateString(Date date, String format, TimeZone tz) {
+        SimpleDateFormat fmt = new SimpleDateFormat(format);
+        if (tz!=null) fmt.setTimeZone(tz);
+        return fmt.format(date);
+    }
+    /** as {@link #makeDateString(Date, String)} using {@link #DATE_FORMAT_PREFERRED_W_TZ} */
+    public static String makeDateString(Calendar date) {
+        return makeDateString(date.getTime(), DATE_FORMAT_PREFERRED_W_TZ);
+    }
+    /** as {@link #makeDateString(Date, String, TimeZone)} for the time zone of the given calendar object */
+    public static String makeDateString(Calendar date, String format) {
+        return makeDateString(date.getTime(), format, date.getTimeZone());
     }
 
     public static Function<Long, String> toDateString() { return dateString; }
@@ -472,48 +500,72 @@ public class Time {
         }
     }
 
+    public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) {
+        GregorianCalendar cal = new GregorianCalendar();
+        cal.setTimeInMillis(timestamp);
+        return cal;
+    }
+
+    public static Calendar newCalendarFromDate(Date date) {
+        return newCalendarFromMillisSinceEpochUtc(date.getTime());
+    }
+    
+    /** As {@link #parseCalendar(String)} but returning a {@link Date},
+     * (i.e. a record where the time zone has been applied and forgotten). */
+    public static Date parseDate(String input) {
+        if (input==null) return null;
+        return parseCalendarMaybe(input).get().getTime();
+    }
+
     /** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html, 
-     * e.g. 2015-06-15 16:00:00 +0000. Millis since eopch UTC is also supported.
+     * e.g. 2015-06-15 16:00:00 +0000.
+     * <p>
+     * Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium,
+     * but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted.
+     * (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.) 
+     * <p>
      * Other formats including locale-specific variants, e.g. recognising month names,
      * are supported but this may vary from platform to platform and may change between versions. */
-    public static Date parseDate(String input) {
+    public static Calendar parseCalendar(String input) {
         if (input==null) return null;
-        return parseDateMaybe(input).get();
+        return parseCalendarMaybe(input).get();
     }
     
-    /** as {@link #parseDate(String)} but returning a {@link Maybe} rather than throwing or returning null */
-    public static Maybe<Date> parseDateMaybe(String input) {
+    /** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */
+    public static Maybe<Calendar> parseCalendarMaybe(String input) {
         if (input==null) return Maybe.absent("value is null");
         input = input.trim();
-        Maybe<Date> result;
+        Maybe<Calendar> result;
 
-        result = parseDateUtc(input);
+        result = parseCalendarUtc(input);
         if (result.isPresent()) return result;
 
-        result = parseDateSimpleFlexibleFormatParser(input);
+        result = parseCalendarSimpleFlexibleFormatParser(input);
         if (result.isPresent()) return result;
         // return the error from this method
-        Maybe<Date> returnResult = result;
+        Maybe<Calendar> 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));
+        result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
         if (result.isPresent()) return result;
-        result = parseDateDefaultParse(input);
+        result = parseCalendarDefaultParse(input);
         if (result.isPresent()) return result;
 
         return returnResult;
     }
 
     @SuppressWarnings("deprecation")
-    private static Maybe<Date> parseDateDefaultParse(String input) {
+    private static Maybe<Calendar> parseCalendarDefaultParse(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));
+                GregorianCalendar c = new GregorianCalendar();
+                c.setTimeInMillis(ms);
+                return Maybe.of((Calendar)c);
             }
         } catch (Exception e) {
             Exceptions.propagateIfFatal(e);
@@ -521,12 +573,16 @@ public class Time {
         return Maybe.absent();
     }
 
-    private static Maybe<Date> parseDateUtc(String input) {
+    private static Maybe<Calendar> parseCalendarUtc(String input) {
+        input = input.trim();
         if (input.matches("\\d+")) {
-            Maybe<Date> result = Maybe.of(new Date(Long.parseLong(input)));
+            if ("0".equals(input)) {
+                // accept 0 as epoch UTC
+                return Maybe.of(newCalendarFromMillisSinceEpochUtc(0));
+            }
+            Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input)));
             if (result.isPresent()) {
-                @SuppressWarnings("deprecation")
-                int year = result.get().getYear();
+                int year = result.get().get(Calendar.YEAR);
                 if (year >= 2000 && year < 2200) {
                     // only applicable for dates in this century
                     return result;
@@ -606,7 +662,7 @@ public class Time {
     }
     
     @SuppressWarnings("deprecation")
-    private static Maybe<Date> parseDateSimpleFlexibleFormatParser(String input) {
+    private static Maybe<Calendar> parseCalendarSimpleFlexibleFormatParser(String input) {
         input = input.trim();
 
         String[] DATE_PATTERNS = new String[] {
@@ -757,14 +813,16 @@ public class Time {
                 }
             }
             
-            return Maybe.of(result.getTime());
+            return Maybe.of(result);
         }
         return Maybe.absent("Unknown date format '"+input+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
     }
     
     public static TimeZone getTimeZone(String code) {
         if (code.indexOf('/')==-1) {
-            if ("Z".equals(code)) return getTimeZone("UTC");
+            if ("Z".equals(code)) return TIME_ZONE_UTC;
+            if ("UTC".equals(code)) return TIME_ZONE_UTC;
+            if ("GMT".equals(code)) return TIME_ZONE_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");
@@ -872,22 +930,22 @@ public class Time {
      * <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
+     * @deprecated since 0.7.0 use {@link #parseCalendar(String)} for general or {@link #parseCalendarFormat(String, DateFormat)} for a format,
+     * plus {@link #parseCalendarUtc(String)} if you want to accept UTC
      */
     public static Date parseDateString(String dateString, DateFormat format) {
-        Maybe<Date> r = parseDateFormat(dateString, format);
-        if (r.isPresent()) return r.get();
+        Maybe<Calendar> r = parseCalendarFormat(dateString, format);
+        if (r.isPresent()) return r.get().getTime();
         
-        r = parseDateUtc(dateString);
-        if (r.isPresent()) return r.get();
+        r = parseCalendarUtc(dateString);
+        if (r.isPresent()) return r.get().getTime();
 
         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<Calendar> parseCalendarFormat(String dateString, String format) {
+        return parseCalendarFormat(dateString, new SimpleDateFormat(format));
     }
-    public static Maybe<Date> parseDateFormat(String dateString, DateFormat format) {
+    public static Maybe<Calendar> parseCalendarFormat(String dateString, DateFormat format) {
         if (dateString == null) { 
             throw new NumberFormatException("GeneralHelper.parseDateString cannot parse a null string");
         }
@@ -898,7 +956,7 @@ public class Time {
         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);
+            return Maybe.of(newCalendarFromDate(result));
         }
         if (log.isTraceEnabled()) log.trace("Could not parse date "+dateString+" using format "+format+": "+p);
         return Maybe.absent();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/51ba0aaf/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 8ad9caa..5f16d5e 100644
--- a/utils/common/src/test/java/brooklyn/util/time/TimeTest.java
+++ b/utils/common/src/test/java/brooklyn/util/time/TimeTest.java
@@ -150,6 +150,17 @@ public class TimeTest {
         Assert.assertFalse(Time.hasElapsedSince(aFewSecondsAgo, Duration.TEN_SECONDS));
         Assert.assertTrue(Time.hasElapsedSince(-1, Duration.TEN_SECONDS));
     }
+    
+    @Test
+    public void testMakeDateString() {
+        String in1 = "2015-06-15T12:34:56";
+        Date d1 = Time.parseDate(in1);
+        Assert.assertEquals(Time.makeDateString(d1), in1.replace('T', ' ')+".000");
+        
+        String in2 = "2015-06-15T12:34:56Z";
+        Date d2 = Time.parseDate(in2);
+        Assert.assertEquals(Time.makeDateString(d2, Time.DATE_FORMAT_ISO8601, Time.getTimeZone("UTC")), in1+".000+0000");
+    }
 
     @Test(groups="Integration")  //because it depends on TZ's set up and parsing months
     public void testTimeZones() {
@@ -238,19 +249,34 @@ public class TimeTest {
         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");
         assertDatesParseToEqual("20150604T080012.345Z", "2015-06-04-080012.345+0000");
         assertDatesParseToEqual("20150604t080012.345 Z", "2015-06-04-080012.345+0000");
-        
+
+        // millis parse, and zero is epoch, but numbers which look like a date or datetime take priority
+        assertDatesParseToEqual("0", "1970-1-1 UTC");
+        assertDatesParseToEqual("20150604", "2015-06-04");
+        assertDatesParseToEqual(""+Time.parseDate("20150604").getTime(), "2015-06-04");
+        assertDatesParseToEqual("20150604080012", "2015-06-04-080012");
+        assertDatesParseToEqual("0", "1970-1-1 UTC");
+
+        // leap year
+        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());
+
         // 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");
         
+        // *calendar* parse includes time zone
+        Assert.assertEquals(Time.makeDateString(Time.parseCalendar("20150604 08:00:12.345a +0100"),
+            Time.DATE_FORMAT_ISO8601), "2015-06-04T08:00:12.345+0100");
+        Assert.assertEquals(Time.makeDateString(Time.parseCalendar("20150604 08:00:12.345a "+Time.TIME_ZONE_UTC.getID()),
+            Time.DATE_FORMAT_ISO8601), "2015-06-04T08:00:12.345+0000");
+        
         // accept month in words
         if (integration) {
             assertDatesParseToEqual("2015-Dec-1", "2015-12-01-0000");