killbill-aplcache

context: handle DST gap When converting a LocalDate to DateTime,

2/16/2016 12:04:12 PM

Details

diff --git a/api/src/main/java/org/killbill/billing/callcontext/TimeAwareContext.java b/api/src/main/java/org/killbill/billing/callcontext/TimeAwareContext.java
index c5c88e9..ca38a75 100644
--- a/api/src/main/java/org/killbill/billing/callcontext/TimeAwareContext.java
+++ b/api/src/main/java/org/killbill/billing/callcontext/TimeAwareContext.java
@@ -21,6 +21,7 @@ import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
+import org.joda.time.IllegalInstantException;
 import org.joda.time.LocalDate;
 import org.joda.time.LocalTime;
 
@@ -53,13 +54,19 @@ public class TimeAwareContext {
     public DateTime toUTCDateTime(final LocalDate localDate) {
         validateContext();
 
-        final DateTime targetDateTime = new DateTime(localDate.getYear(),
-                                                     localDate.getMonthOfYear(),
-                                                     localDate.getDayOfMonth(),
-                                                     getReferenceTime().getHourOfDay(),
-                                                     getReferenceTime().getMinuteOfHour(),
-                                                     getReferenceTime().getSecondOfMinute(),
-                                                     getFixedOffsetTimeZone());
+         DateTime targetDateTime;
+        try {
+            targetDateTime = new DateTime(localDate.getYear(),
+                                          localDate.getMonthOfYear(),
+                                          localDate.getDayOfMonth(),
+                                          getReferenceTime().getHourOfDay(),
+                                          getReferenceTime().getMinuteOfHour(),
+                                          getReferenceTime().getSecondOfMinute(),
+                                          getFixedOffsetTimeZone());
+        } catch (final IllegalInstantException e) {
+            // DST gap
+            targetDateTime = localDate.toDateTimeAtStartOfDay(getFixedOffsetTimeZone());
+        }
 
         return toUTCDateTime(targetDateTime);
     }
diff --git a/util/src/test/java/org/killbill/billing/util/callcontext/TestTimeAwareContext.java b/util/src/test/java/org/killbill/billing/util/callcontext/TestTimeAwareContext.java
index 14d116d..49ecb00 100644
--- a/util/src/test/java/org/killbill/billing/util/callcontext/TestTimeAwareContext.java
+++ b/util/src/test/java/org/killbill/billing/util/callcontext/TestTimeAwareContext.java
@@ -23,11 +23,15 @@ import org.joda.time.LocalDate;
 import org.joda.time.format.DateTimeFormatter;
 import org.joda.time.format.ISODateTimeFormat;
 import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.TimeAwareContext;
 import org.killbill.billing.mock.MockAccountBuilder;
 import org.killbill.billing.util.UtilTestSuiteNoDB;
 import org.killbill.billing.util.account.AccountDateTimeUtils;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.ImmutableList;
+
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
@@ -139,6 +143,50 @@ public class TestTimeAwareContext extends UtilTestSuiteNoDB {
         assertEquals(internalCallContext.toLocalDate(dateTime3), new LocalDate("2015-11-30"));
     }
 
+    @Test(groups = "fast")
+    public void testIdempotencyOfDatesManipulation() throws Exception {
+        final ImmutableList.Builder<DateTimeZone> dateTimeZoneBuilder = ImmutableList.<DateTimeZone>builder();
+        dateTimeZoneBuilder.add(DateTimeZone.forID("HST"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("PST8PDT"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("MST"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("CST6CDT"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("EST"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("Brazil/DeNoronha"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("UTC"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("CET"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("Europe/Istanbul"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("Singapore"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("Japan"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("Australia/Sydney"));
+        dateTimeZoneBuilder.add(DateTimeZone.forID("Pacific/Tongatapu"));
+        final Iterable<DateTimeZone> dateTimeZones = dateTimeZoneBuilder.build();
+
+        final ImmutableList.Builder<DateTime> referenceDateTimeBuilder = ImmutableList.<DateTime>builder();
+        referenceDateTimeBuilder.add(new DateTime(2012, 1, 1, 1, 1, 1, DateTimeZone.UTC));
+        referenceDateTimeBuilder.add(new DateTime(2012, 3, 15, 12, 42, 0, DateTimeZone.forID("PST8PDT")));
+        referenceDateTimeBuilder.add(new DateTime(2012, 11, 15, 12, 42, 0, DateTimeZone.forID("PST8PDT")));
+        final Iterable<DateTime> referenceDateTimes = referenceDateTimeBuilder.build();
+
+        DateTime currentDateTime = new DateTime(2015, 1, 1, 1, 1, DateTimeZone.UTC);
+        final DateTime endDateTime = new DateTime(2020, 1, 1, 1, 1, DateTimeZone.UTC);
+        while (currentDateTime.compareTo(endDateTime) <= 0) {
+            for (final DateTimeZone dateTimeZone : dateTimeZones) {
+                for (final DateTime referenceDateTime : referenceDateTimes) {
+                    final TimeAwareContext timeAwareContext = new TimeAwareContext(dateTimeZone, referenceDateTime);
+
+                    final LocalDate computedLocalDate = timeAwareContext.toLocalDate(currentDateTime);
+                    final DateTime computedDateTime = timeAwareContext.toUTCDateTime(computedLocalDate);
+                    final LocalDate computedLocalDate2 = timeAwareContext.toLocalDate(computedDateTime);
+
+                    final String msg = String.format("currentDateTime=%s, localDate=%s, dateTime=%s, dateTimeZone=%s, referenceDateTime=%s", currentDateTime, computedLocalDate, computedDateTime, dateTimeZone, referenceDateTime);
+                    Assert.assertEquals(computedLocalDate2, computedLocalDate, msg);
+                }
+            }
+
+            currentDateTime = currentDateTime.plusHours(1);
+        }
+    }
+
     private void refreshCallContext(final DateTime effectiveDateTime, final DateTimeZone timeZone) {
         final Account account = new MockAccountBuilder().timeZone(timeZone)
                                                         .createdDate(effectiveDateTime)