java.time support for FreeMarker

FJ8 (freemarker-java-8) is a Java library that adds java.time api support to FreeMarker. It is easy to add to your codebase, and very easy to use.

Basically this library allows you to format and print values from java.time classes within FreeMarker templates. As a bonus you also get some comparison functions.

It is not a perfect solution as FreeMarker doesn’t support custom built-ins. Hopefully future versions of FreeMarker will add native support, but it doesn't look promising (http://freemarker.org/contribute.html).

Basically this library allows you to format java.time types within your templates, using the new java.time.format.DateTimeFormatter.

Table of content

Installation

You need Java 8 or higher. FJ8 is tested on Freemarker 2.3.23, and should at least work fine for all 2.3.x versions.

Maven

<dependency>
    <groupId>no.api.freemarker</groupId>
    <artifactId>freemarker-java8</artifactId>
    <version>2.0.0</version>
</dependency>

Gradle

implementation 'no.api.freemarker:freemarker-java8:2.0.0'

Setup

FJ8 extends the DefaultObjectWrapper to add support for the java.time classes. All you need to do is to replace the default object wrapper with the FJ8 implementation in your FreeMarker Configuration object.

this.configuration = new Configuration(); // Or get the configuration from your framework like DropWizard or Spring Boot.
this.configuration.setObjectWrapper(new Java8ObjectWrapper(Configuration.VERSION_2_3_23));

Spring setup

This is how you can add FJ8 to your FreeMarker configuration in Spring / Spring Boot.

package com.example.demo;

import no.api.freemarker.java8.Java8ObjectWrapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

@Configuration
public class FreemarkerConfig implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean instanceof FreeMarkerConfigurer) {
            FreeMarkerConfigurer configurer = (FreeMarkerConfigurer) bean;
            configurer.getConfiguration().setObjectWrapper(new Java8ObjectWrapper(freemarker.template.Configuration.getVersion()));
        }
        return bean;
    }
}

Thanks to Desson Ariawan for the example

Upgrade from 1.3 to 2.0

The 2.0 release addresses two major issues reported by users (#18/#16). It also introduces a new feature for manipulating time (#28).

The upgrade itself is nothing else than changing the version in your build configuration (pom.xml or something else). However if you need to stick to the old behaviour on how time zones are treated when formatting ZonedDateTime objects, then you need to add a second argument to Java8ObjectWrapper upon initialization:

configuration.setObjectWrapper(
    new Java8ObjectWrapper(VERSION_2_3_23, new EnvironmentTimeStrategy()
);

Usage

Formatting java.time classes

All format methods uses the java.time.format.DateTimeFormatter for formatting.

:ballot_box_with_check: java.time.Clock

This is a simple implementation where format just prints the toString() value of the object.

Methods

Example

${myclock.format()}

java.time.Duration

Gives access to the Duration values.

Methods

Example

${myduration.seconds}
${myduration.nano}

:ballot_box_with_check: java.time.Instant

This is a simple implementation where format just prints the toString() value of the object.

Methods

Example

${myinstant.format()}

:ballot_box_with_check: java.time.LocalDate

Allows you to print a LocalDate on a default pattern, by providing a custom pattern or a builtin format style.

Methods

Example

${mylocaldate.format()}
${mylocaldate.format('yyyy MM dd')}
${mylocaldate.format('FULL_DATE')}

:ballot_box_with_check: java.time.LocalDateTime

Allows you to print a LocalDateTime on a default pattern, by providing a custom pattern or a builtin format style.

Methods

Example

${mylocaldatetime.format()}
${mylocaldatetime.format('yyyy-MM-dd HH : mm : ss')}
${mylocaldatetime.format('MEDIUM_DATETIME')}

:ballot_box_with_check: java.time.LocalTime

Allows you to print a LocalTime on a default pattern, by providing a custom pattern or a builtin format style.

Methods

Example

${mylocaltime.format()}
${mylocaltime.format('HH : mm : ss')}
${mylocaltime.format('SHORT_TIME')}

:ballot_box_with_check: java.time.MonthDay

Allows you to print a MonthDay on a default pattern or by providing a custom pattern.

Methods

Example

${mymonthday.format()}
${mymonthday.format('MM dd')}

:ballot_box_with_check: java.time.OffsetDateTime

Allows you to print a OffsetDateTime on a default pattern, by providing a custom pattern or a builtin format style.

Methods

Example

${myoffsetdatetime.format()}
${myoffsetdatetime.format('yyyy MM dd HH mm ss')}
${myoffsetdatetime.format('FULL_DATETIME')}

:ballot_box_with_check: java.time.OffsetTime

Allows you to print a OffsetTime on a default pattern, by providing a custom pattern or a builtin format style.

Methods

Example

${myoffsettime.format()}
${myoffsettime.format('HH mm ss')}
${myoffsettime.format('MEDIUM_TIME')}

:ballot_box_with_check: java.time.Period

Provides access to the values of the a Period object within your template.

Methods

Example

${myperiod.days}
${myperiod.months}
${myperiod.years}

:ballot_box_with_check: java.time.Year

Allows you to print a Year on a default pattern or by providing a custom pattern.

Methods

Example

${myyear.format()}
${myyear.format('yyyy')}

:ballot_box_with_check: java.time.YearMonth

Allows you to print a YearMonth on a default pattern or by providing a custom pattern.

Methods

Example

${myyear.format()}
${myyear.format('yyyy MM')}

:ballot_box_with_check: java.time.ZonedDateTime

Allows you to print a YearMonth on a default pattern/timezone or by providing a custom pattern.

Methods

Example

${myzoneddatetime.format()}
${myzoneddatetime.format('yyyy-MM-dd Z')}
${myzoneddatetime.format('yyyy-MM-dd Z', 'Asia/Seoul')}

Notice

When a zone is not set, the formatter will use the zone found in the ZonedDateTime object itself. This behaviour can be changed if you want to. Scenarious where that might come in handy could be when you always wants to convert the timezone into your local timezone.

Java8ObjectMappernow takes a second argument where you can choose one of four strategies for the time zone used when formatting a ZonedDateTime:

Example:

new Java8ObjectWrapper(VERSION_2_3_23, new EnvironmentZonedDateTimeStrategy());
// or
new Java8ObjectWrapper(VERSION_2_3_23, new StaticZonedDateTimeStrategy(ZoneId.of("Europe/Oslo")));

:ballot_box_with_check: java.time.ZonedId

Prints the ZoneId display name. You can override the textstyle with one of these values [FULL, FULL_STANDALONE, SHORT, SHORT_STANDALONE, NARROW and NARROW_STANDALONE]. You can also override the locale, but Java only seems to have locale support for a few languages.

Methods

Example

${myzoneid.format()}
${myzoneid.format('short')}
${myzoneid.format('short', 'no-NO')}

:ballot_box_with_check: java.time.ZonedOffset

Prints the ZoneOffset display name. You can override the textstyle with one of these values [FULL, FULL_STANDALONE, SHORT, SHORT_STANDALONE, NARROW and NARROW_STANDALONE]. You can also override the locale, but Java only seems to have locale support for a few languages.

Methods

Example

${myzoneoffset.format()}
${myzoneoffset.format('short')}

Comparison

:ballot_box_with_check: java.time.LocalDate

Can compare two LocalDate objects for equality.

Methods

Example

${localDate.isEqual(anotherlocalDate)}
${localDate.isAfter(anotherlocalDate)}
${localDate.isBefore(anotherlocalDate)}

:ballot_box_with_check: java.time.LocalDateTime

Can compare two LocalDateTime objects for equality.

Methods

Example

${localDateTime.isEqual(anotherlocalDateTime)}
${localDateTime.isAfter(anotherlocalDateTime)}
${localDateTime.isBefore(anotherlocalDateTime)}

:ballot_box_with_check: java.time.LocalTime

Can compare two LocalTime objects for equality.

Methods

Example

${localTime.isEqual(anotherlocalTime)}
${localTime.isAfter(anotherlocalTime)}
${localTime.isBefore(anotherlocalTime)}

Manipulating time

:ballot_box_with_check: java.time.temporal.Temporal

Can create a new Temporal object with specified time difference from the original object, supporting

java.time.Instant, 
java.time.LocalDate, 
java.time.LocalDateTime, 
java.time.LocalTime, 
java.time.OffsetDateTime, 
java.time.OffsetTime, 
java.time.Year, 
java.time.YearMonth, 
java.time.ZonedDateTime

Methods

Example

${localDateTime.plusMonths(1).plus.Hours(-2).plusMinutes(5).plusSeconds(30).format()}

Notice

Recently this repository was moved from the Amedia organisation to my private account on Github. The reason behind this is that I recently left Amedia after 13 years (!) and that they let me take this project with me. The package naming will however stay the same.