FJ8 (freemarker-java-8) is a Java library that adds FreeMarker support for the java.time api, introduced in Java 8 (and newer versions). 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.
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.
<dependency>
<groupId>no.api.freemarker</groupId>
<artifactId>freemarker-java8</artifactId>
<version>2.0.0</version>
</dependency>
implementation 'no.api.freemarker:freemarker-java8:2.0.0'
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));
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
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()
);
All format methods uses the java.time.format.DateTimeFormatter for formatting.
This is a simple implementation where format just prints the toString() value of the object.
- format()
${myclock.format()}
Gives access to the Duration values.
- nano()
- seconds()
${myduration.seconds}
${myduration.nano}
This is a simple implementation where format just prints the toString() value of the object.
- format()
- format(pattern)
${myinstant.format()}
Allows you to print a LocalDate on a default pattern, by providing a custom pattern or a builtin format style.
- format()
${mylocaldate.format()}
${mylocaldate.format('yyyy MM dd')}
${mylocaldate.format('FULL_DATE')}
Allows you to print a LocalDateTime on a default pattern, by providing a custom pattern or a builtin format style.
- format()
- format(pattern)
${mylocaldatetime.format()}
${mylocaldatetime.format('yyyy-MM-dd HH : mm : ss')}
${mylocaldatetime.format('MEDIUM_DATETIME')}
Allows you to print a LocalTime on a default pattern, by providing a custom pattern or a builtin format style.
- format()
- format(pattern)
${mylocaltime.format()}
${mylocaltime.format('HH : mm : ss')}
${mylocaltime.format('SHORT_TIME')}
Allows you to print a MonthDay on a default pattern or by providing a custom pattern.
- format()
- format(pattern)
${mymonthday.format()}
${mymonthday.format('MM dd')}
Allows you to print a OffsetDateTime on a default pattern, by providing a custom pattern or a builtin format style.
- format()
- format(pattern)
${myoffsetdatetime.format()}
${myoffsetdatetime.format('yyyy MM dd HH mm ss')}
${myoffsetdatetime.format('FULL_DATETIME')}
Allows you to print a OffsetTime on a default pattern, by providing a custom pattern or a builtin format style.
- format()
- format(pattern)
${myoffsettime.format()}
${myoffsettime.format('HH mm ss')}
${myoffsettime.format('MEDIUM_TIME')}
Provides access to the values of the a Period object within your template.
- days()
- months()
- years()
${myperiod.days}
${myperiod.months}
${myperiod.years}
Allows you to print a Year on a default pattern or by providing a custom pattern.
- format()
- format(pattern)
${myyear.format()}
${myyear.format('yyyy')}
Allows you to print a YearMonth on a default pattern or by providing a custom pattern.
- format()
- format(pattern)
${myyear.format()}
${myyear.format('yyyy MM')}
Allows you to print a YearMonth on a default pattern/timezone or by providing a custom pattern.
- format()
- format(pattern)
- format(pattern, zone)
${myzoneddatetime.format()}
${myzoneddatetime.format('yyyy-MM-dd Z')}
${myzoneddatetime.format('yyyy-MM-dd Z', 'Asia/Seoul')}
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.
Java8ObjectMapper
now takes a second argument where you can choose one of four strategies for
the time zone used when formatting a ZonedDateTime:
- EnviromentZonedDateTimeStrategy - Will convert the time zone into the one currently set within Freemarker.
- KeepingZonedDateTimeStrategy - Will use the zone from the ZonedDateTime object itself (DEFAULT)
- SystemZonedDateTimeStrategy - Will convert the time zone into ZoneId.systemDefault().
- StaticSystemZoneDateTimeStrategy - Will use the time zone set when creating this strategy.
Example:
new Java8ObjectWrapper(VERSION_2_3_23, new EnvironmentZonedDateTimeStrategy());
// or
new Java8ObjectWrapper(VERSION_2_3_23, new StaticZonedDateTimeStrategy(ZoneId.of("Europe/Oslo")));
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.
- format()
- format(textStyle)
- format(textstyle, locale)
${myzoneid.format()}
${myzoneid.format('short')}
${myzoneid.format('short', 'no-NO')}
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.
- format()
- format(textStyle)
${myzoneoffset.format()}
${myzoneoffset.format('short')}
Can compare two LocalDate objects for equality.
- isEqual(localDate)
- isAfter(localDate)
- isBefore(localDate)
${localDate.isEqual(anotherlocalDate)}
${localDate.isAfter(anotherlocalDate)}
${localDate.isBefore(anotherlocalDate)}
Can compare two LocalDateTime objects for equality.
- isEqual(localDateTime)
- isAfter(localDateTime)
- isBefore(localDateTime)
${localDateTime.isEqual(anotherlocalDateTime)}
${localDateTime.isAfter(anotherlocalDateTime)}
${localDateTime.isBefore(anotherlocalDateTime)}
Can compare two LocalTime objects for equality.
- isEqual(localDateTime)
- isAfter(localDateTime)
- isBefore(localDateTime)
${localTime.isEqual(anotherlocalTime)}
${localTime.isAfter(anotherlocalTime)}
${localTime.isBefore(anotherlocalTime)}
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
- plusSeconds()
- plusMinutes()
- plusDays()
- plusWeeks()
- plusMonths()
- plusYears()
${localDateTime.plusMonths(1).plus.Hours(-2).plusMinutes(5).plusSeconds(30).format()}
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.