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 <[email protected]> Authored: Tue Jun 9 11:17:22 2015 +0100 Committer: Alex Heneveld <[email protected]> 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");
