diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index ad527458c..d854d21e7 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -25,6 +25,8 @@ import com.hubspot.jinjava.interpret.JinjavaInterpreterFactory; import com.hubspot.jinjava.mode.DefaultExecutionMode; import com.hubspot.jinjava.mode.ExecutionMode; +import com.hubspot.jinjava.objects.date.CurrentDateTimeProvider; +import com.hubspot.jinjava.objects.date.DateTimeProvider; import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; import com.hubspot.jinjava.tree.parse.TokenScannerSymbols; @@ -62,6 +64,7 @@ public class JinjavaConfig { private final int rangeLimit; private final int maxNumDeferredTokens; private final InterpreterFactory interpreterFactory; + private final DateTimeProvider dateTimeProvider; private TokenScannerSymbols tokenScannerSymbols; private final ELResolver elResolver; private final ExecutionMode executionMode; @@ -121,6 +124,7 @@ private JinjavaConfig(Builder builder) { elResolver = builder.elResolver; executionMode = builder.executionMode; legacyOverrides = builder.legacyOverrides; + dateTimeProvider = builder.dateTimeProvider; enablePreciseDivideFilter = builder.enablePreciseDivideFilter; objectMapper = builder.objectMapper; } @@ -241,6 +245,10 @@ public boolean getEnablePreciseDivideFilter() { return enablePreciseDivideFilter; } + public DateTimeProvider getDateTimeProvider() { + return dateTimeProvider; + } + public static class Builder { private Charset charset = StandardCharsets.UTF_8; private Locale locale = Locale.ENGLISH; @@ -258,6 +266,7 @@ public static class Builder { private boolean nestedInterpretationEnabled = true; private RandomNumberGeneratorStrategy randomNumberGeneratorStrategy = RandomNumberGeneratorStrategy.THREAD_LOCAL; + private DateTimeProvider dateTimeProvider = new CurrentDateTimeProvider(); private boolean validationMode = false; private long maxStringLength = 0; private int rangeLimit = DEFAULT_RANGE_LIMIT; @@ -306,6 +315,11 @@ public Builder withRandomNumberGeneratorStrategy( return this; } + public Builder withDateTimeProvider(DateTimeProvider dateTimeProvider) { + this.dateTimeProvider = dateTimeProvider; + return this; + } + public Builder withTrimBlocks(boolean trimBlocks) { this.trimBlocks = trimBlocks; return this; diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index a3a392019..7ede5f016 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -156,9 +156,9 @@ public void addBlock(String name, BlockInfo blockInfo) { /** * Creates a new variable scope, extending from the current scope. Allows you to create a nested * contextual scope which can override variables from higher levels. - * + *

* Should be used in a try/finally context, similar to lock-use patterns: - * + *

* * interpreter.enterScope(); * try (interpreter.enterScope()) { diff --git a/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java b/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java index 94c2b2c07..e620d2988 100644 --- a/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java +++ b/src/main/java/com/hubspot/jinjava/lib/fn/Functions.java @@ -13,6 +13,7 @@ import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.mode.ExecutionMode; import com.hubspot.jinjava.objects.Namespace; +import com.hubspot.jinjava.objects.date.DateTimeProvider; import com.hubspot.jinjava.objects.date.PyishDate; import com.hubspot.jinjava.objects.date.StrftimeFormatter; import com.hubspot.jinjava.tree.Node; @@ -245,7 +246,14 @@ public static ZonedDateTime getDateTimeArg(Object var, ZoneId zoneOffset) { JinjavaInterpreter.getCurrent().getPosition() ); } - d = ZonedDateTime.now(zoneOffset); + long currentMillis = JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::getDateTimeProvider) + .map(DateTimeProvider::getCurrentTimeMillis) + .orElse(System.currentTimeMillis()); + + d = ZonedDateTime.ofInstant(Instant.ofEpochMilli(currentMillis), zoneOffset); } else if (var instanceof Number) { d = ZonedDateTime.ofInstant( diff --git a/src/main/java/com/hubspot/jinjava/objects/date/CurrentDateTimeProvider.java b/src/main/java/com/hubspot/jinjava/objects/date/CurrentDateTimeProvider.java new file mode 100644 index 000000000..937d4d00a --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/date/CurrentDateTimeProvider.java @@ -0,0 +1,9 @@ +package com.hubspot.jinjava.objects.date; + +public class CurrentDateTimeProvider implements DateTimeProvider { + + @Override + public long getCurrentTimeMillis() { + return System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/hubspot/jinjava/objects/date/DateTimeProvider.java b/src/main/java/com/hubspot/jinjava/objects/date/DateTimeProvider.java new file mode 100644 index 000000000..ca6c53c9e --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/date/DateTimeProvider.java @@ -0,0 +1,5 @@ +package com.hubspot.jinjava.objects.date; + +public interface DateTimeProvider { + long getCurrentTimeMillis(); +} diff --git a/src/main/java/com/hubspot/jinjava/objects/date/FixedDateTimeProvider.java b/src/main/java/com/hubspot/jinjava/objects/date/FixedDateTimeProvider.java new file mode 100644 index 000000000..75bc71ce8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/objects/date/FixedDateTimeProvider.java @@ -0,0 +1,14 @@ +package com.hubspot.jinjava.objects.date; + +public class FixedDateTimeProvider implements DateTimeProvider { + private long currentTimeMillis; + + public FixedDateTimeProvider(long currentTimeMillis) { + this.currentTimeMillis = currentTimeMillis; + } + + @Override + public long getCurrentTimeMillis() { + return currentTimeMillis; + } +} diff --git a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java index 9ffd07e5e..ee5722e45 100644 --- a/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java +++ b/src/main/java/com/hubspot/jinjava/objects/date/PyishDate.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.objects.date; +import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.PyWrapper; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; @@ -50,7 +51,17 @@ public PyishDate(Long epochMillis) { this( ZonedDateTime.ofInstant( Instant.ofEpochMilli( - Optional.ofNullable(epochMillis).orElseGet(System::currentTimeMillis) + Optional + .ofNullable(epochMillis) + .orElseGet( + () -> + JinjavaInterpreter + .getCurrentMaybe() + .map(JinjavaInterpreter::getConfig) + .map(JinjavaConfig::getDateTimeProvider) + .map(DateTimeProvider::getCurrentTimeMillis) + .orElseGet(System::currentTimeMillis) + ) ), ZoneOffset.UTC ) diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java index fc28df5e9..e52cea3de 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/UnixTimestampFilterTest.java @@ -20,12 +20,12 @@ public void setup() { } @After - public void tearDown() throws Exception { + public void tearDown() { assertThat(interpreter.getErrorsCopy()).isEmpty(); } @Test - public void itRendersFromDate() throws Exception { + public void itRendersFromDate() { assertThat(interpreter.renderFlat("{{ d|unixtimestamp }}")).isEqualTo(timestamp); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java index abb3830c4..63cbd9fcb 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/TodayFunctionTest.java @@ -10,12 +10,15 @@ import com.hubspot.jinjava.interpret.InvalidArgumentException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.mode.EagerExecutionMode; +import com.hubspot.jinjava.objects.date.FixedDateTimeProvider; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import org.junit.Test; public class TodayFunctionTest extends BaseInterpretingTest { + private static final String ZONE_NAME = "America/New_York"; + private static final ZoneId ZONE_ID = ZoneId.of(ZONE_NAME); @Test public void itDefaultsToUtcTimezone() { @@ -23,10 +26,32 @@ public void itDefaultsToUtcTimezone() { assertThat(zonedDateTime.getZone()).isEqualTo(ZoneOffset.UTC); } + @Test + public void itUsesFixedDateTimeProvider() { + long ts = 1233333414223L; + + JinjavaInterpreter.pushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + JinjavaConfig + .newBuilder() + .withDateTimeProvider(new FixedDateTimeProvider(ts)) + .build() + ) + ); + try { + assertThat(Functions.today(ZONE_NAME)) + .isEqualTo(ZonedDateTime.of(2009, 1, 30, 0, 0, 0, 0, ZONE_ID)); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + @Test public void itParsesTimezones() { - ZonedDateTime zonedDateTime = Functions.today("America/New_York"); - assertThat(zonedDateTime.getZone()).isEqualTo(ZoneId.of("America/New_York")); + ZonedDateTime zonedDateTime = Functions.today(ZONE_NAME); + assertThat(zonedDateTime.getZone()).isEqualTo(ZONE_ID); } @Test(expected = InvalidArgumentException.class) @@ -52,7 +77,7 @@ public void itDefersWhenExecutingEagerly() { ) ); try { - Functions.today("America/New_York"); + Functions.today(ZONE_NAME); } finally { JinjavaInterpreter.popCurrent(); } diff --git a/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java b/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java index e7cb1e3b9..de494fd85 100644 --- a/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/fn/UnixTimestampFunctionTest.java @@ -8,7 +8,9 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.mode.EagerExecutionMode; +import com.hubspot.jinjava.objects.date.FixedDateTimeProvider; import java.time.ZonedDateTime; +import org.assertj.core.data.Offset; import org.junit.Test; public class UnixTimestampFunctionTest { @@ -24,14 +26,29 @@ public void itGetsUnixTimestamps() { .isLessThanOrEqualTo(System.currentTimeMillis()); assertThat(Functions.unixtimestamp(epochMilliseconds)).isEqualTo(epochMilliseconds); assertThat(Functions.unixtimestamp(d)).isEqualTo(epochMilliseconds); - assertThat( - Math.abs( - Functions.unixtimestamp((Object) null) - - ZonedDateTime.now().toEpochSecond() * - 1000 - ) + assertThat(Functions.unixtimestamp((Object) null)) + .isCloseTo(System.currentTimeMillis(), Offset.offset(1000L)); + } + + @Test + public void itUsesFixedDateTimeProvider() { + long ts = 1233333414223L; + + JinjavaInterpreter.pushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + JinjavaConfig + .newBuilder() + .withDateTimeProvider(new FixedDateTimeProvider(ts)) + .build() ) - .isLessThan(1000); + ); + try { + assertThat(Functions.unixtimestamp((Object) null)).isEqualTo(ts); + } finally { + JinjavaInterpreter.popCurrent(); + } } @Test(expected = DeferredValueException.class) diff --git a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java index 0c25fc12f..849c5636c 100644 --- a/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java +++ b/src/test/java/com/hubspot/jinjava/objects/date/PyishDateTest.java @@ -3,8 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.JinjavaConfig; +import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Date; @@ -18,6 +21,28 @@ public void itUsesCurrentTimeWhenNoneProvided() { assertThat(d.toDate()).isCloseTo(new Date(), 10000); } + @Test + public void itUsesDateTimeProviderWhenNoTimeProvided() { + long ts = 123345414223L; + + JinjavaInterpreter.pushCurrent( + new JinjavaInterpreter( + new Jinjava(), + new Context(), + JinjavaConfig + .newBuilder() + .withDateTimeProvider(new FixedDateTimeProvider(ts)) + .build() + ) + ); + try { + PyishDate d = new PyishDate((Long) null); + assertThat(d.toDate()).isEqualTo(Date.from(Instant.ofEpochMilli(ts))); + } finally { + JinjavaInterpreter.popCurrent(); + } + } + @Test public void testStrfmt() { PyishDate d = new PyishDate(ZonedDateTime.parse("2013-11-12T14:15:00+00:00"));