FreshMarker – Groundhog Day

“Well, it’s Groundhog Day… again…”

Phil Connors

The moment is a wonderful thing, but not for software developers, because it keeps on moving. How can you write a test if the value changes with every test run? Functionality has always been an abomination that concealed some form of now. In FreshMarker, for example, this little monster can be found in the built-in variable .now.

This is now: ${.now}

This small fragment of a template has not yet been tested well. An approximation of a good test is to check whether the output time can be found between the start and end of the test. What we need here is the ability to manipulate the time.

 case "now" -> new TemplateZonedDateTime(ZonedDateTime.now(context.getZoneId()))

Previously, the value for the .now variable was generated using the ZonedDateTime#now method. To keep this post short, we can mock the static call with Mokito and we have solved our initial problem. Fortunately for the reader, the initial problem is always just the starting point for a somewhat more in-depth consideration of a wider use-case.

A much more interesting way to manipulate time is to realize a time travel. Not only do we want to ensure in a test that our variable contains the value of ZonedDateTime#now, but the processing in the template engine should work with past and future time values for .now. Typical use cases for such a feature are, of course, tests, but also the regeneration of historical content or the early generation of content required in the future.

Fortunately, the developers of the Java Time API have also thought about this problem and included the Clock into the JDK. The Clock can be used to query the time in form of an Instant or milliseconds. The current Clock can be accessed via Clock#systemUTC and Clock#systemDefaultZone. For the temporal classes such as ZonedDateTime, there are now methods to which a Clock instance can be passed.

Now the question is probably how this will help us at this point. So far, it just looks like another useless indirection. The interesting aspect is that other Clock implementations can be used and the JDK already provides helpful implementations. Some use cases can be solved with the FixedClock. The FixedClock is not a real clock because it always returns the same point in time. Not really a clock, but exactly what we need. In the test, this Clock provides exactly the value we give it because it does not advance in time.

Instant instant = ZonedDateTime.of(1968, 8, 24, 12, 30, 0, 0, ZoneOffset.UTC).toInstant();
Clock clock = Clock.fixed(instant, ZoneOffset.UTC);

ZonedDateTime dateTime = ZonedDateTime.now(clock);

In this case, we use August 24, 1968 at 12:30 p.m. and thus create a FixedClock. Later, we can use it to create our current time as a ZonedDateTime instance.

The conversion for the built-in variable is not much more complex than the previous implementation. Instead of the zone ID, the Clock is returned from the context. The Clock or Clock#systemDefaultZone stored in the TemplateBuilder is used here. In both cases, the Clock is modified with the current zone ID.

 case "now" -> new TemplateZonedDateTime(ZonedDateTime.now(context.getClock()));

So that a Clock can be stored in the TemplateBuilder, we still need a method withClock. A test with this method looks like the following example.

Instant instant = ZonedDateTime.of(1968, 8, 24, 12, 30, 0, 0, ZoneOffset.UTC).toInstant();
templateBuilder = new Configuration().builder().withClock(Clock.fixed(instant, ZoneOffset.UTC));
Template template = templateBuilder.getTemplate("test", "${.now}");
assertEquals("1968-08-24 01:30:00 Europe/Berlin", template.process(Map.of()));
TimeUnit.SECONDS.sleep(5);
assertEquals("1968-08-24 01:30:00 Europe/Berlin", template.process(Map.of()));

A FixedClock was stored in the TemplateBuilder, so the first call returns 1968-08-24 01:30:00 Europe/Berlin. The second call 5 seconds later then returns the identical time.

If you want to use a Clock that moves in the past or future and does not stand still, you can also use the OffsetClock for your use case. It is created with the Clock#offset method.

long currentEpochDay = Localdate.now()
long fluxEpochDay = LocalDate.of(1968, Month.AUGUST, 24);
Clock fluxClock = Clock.offset(clock, Duration.ofDays(fluxEpochDay - currentEpochDay));

In this example, a clock is generated that begins to strike on August 24, 1968. If you want to use your own clock with FreshMarker, you can try it out with version 1.6.9 on Maven Central.

Leave a Comment