/freemarker-java-8

Library that adds java.time support to FreeMarker templates.

Primary LanguageJavaApache License 2.0Apache-2.0

Extending FreeMarker with Java Date and Time API Support

build status

freemarker-java8 is a Java library that extends FreeMarker by adding support for the java.time API. It’s straightforward to integrate into your codebase and easy to use.

Despite the name, the library is compatible with Java 8 and all newer versions.

While the library is rarely updated, it’s still maintained and supported. If you have any questions or issues, please don’t hesitate to open an issue. We also test on new versions and distributions of Java, regularly.

This library enables you to format and display values from java.time classes directly within FreeMarker templates. Additionally, it offers useful comparison and conversion methods to enhance your template logic.

While it provides valuable functionality, it isn't a perfect solution due to FreeMarker's lack of support for custom built-ins. Hopefully, future versions of FreeMarker will include native support, but according to their contribution guidelines, this seems unlikely.

Table of content

Installation

You need Java 8 or higher. Tested on Freemarker 2.3.33, and should at least work fine for all 2.3.x versions.

Maven

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

Gradle

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

Setup

freemarker-java8 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 freemarker-java8 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_33));

Spring setup

This is how you can add freemarker-java8 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;
    }
}

You can also configure it via Spring boot properties like this:

spring.freemarker.settings.object_wrapper=no.api.freemarker.java8.Java8ObjectWrapper(Configuration.VERSION_2_3_33)

This takes advantage of Freemarker Configuration object builder expressions

Upgrade guides

Upgrade information has moved to UPGRADE.md. Also look at CHANGELOG.md for changes in new versions.

Usage

Formatting java.time classes

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

About pattern

Instead of customizing your own pattern in the format methods, you can use one of the predefined styles from:

Examples
${mydate.format('LONG_DATE')}
${mydate.format('ISO_OFFSET_DATE_TIME')}
${mydate.format('yyyy-MM-dd Z')}

When no pattern are given, each java.time class has a default pattern. This default can be overridden in the static DefaultFormatters class, where each class has its own setter method:

no.api.freemarker.java8.time.DefaultFormatters.setClockFormatter(DateTimeFormatter.ofPattern('yyyy MM dd HH:mm:ss'))

About zone

When a method takes zone as argument, it must be a valid Java ZoneId, like Europe/Oslo or -07:00.

The zone argument is only supported for the java.time classes where it makes sense. When a zone is not explicitly given as argument, the formatter will by default use the zone found in the object itself, if it exists. If not, it will pick one based on the active zone strategy.

Th default behaviour can be changed if you like. Sometimes you might want all dates and times to be converted into your local timezone.

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

  • KeepingZoneStrategy - (DEFAULT) Will use the zone from the objects themself. For objects without zone information, ZoneId.systemDefault() is used.
  • EnviromentZoneStrategy - Will use the same time zone as Freemarker itself.
  • SystemZoneStrategy - Will convert the time zone into ZoneId.systemDefault().
  • StaticZoneStrategy - Allows you to explicitly set a ZoneId.
Examples
this.objectWrapper = new Java8ObjectWrapper(VERSION_2_3_33, new KeepingZoneStrategy());
this.objectWrapper = new Java8ObjectWrapper(VERSION_2_3_33, new StaticZoneStrategy(ZoneId.of("Europe/Oslo")));

☑️ java.time.Clock

Methods for formatting java.time.Clock.

Methods
  • format()
  • format(pattern)
  • format(pattern, zone)

When no pattern is specified, ISO_LOCAL_DATE_TIME is used.

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

java.time.Duration

Provided access to a java.time.Duration object values.

Methods
  • nano()
  • seconds()
Examples
${myduration.seconds}
${myduration.nano}

☑️ java.time.Instant

Methods for formatting java.time.Instant.

Methods
  • format()
  • format(pattern)
  • format(pattern, zone)

When no pattern is specified, ISO_LOCAL_DATE_TIME is used.

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

☑️ java.time.LocalDate

Methods for formatting java.time.LocalDate.

Methods
  • format()
  • format(pattern)
Examples
${mylocaldate.format()}
${mylocaldate.format('yyyy MM dd')}
${mylocaldate.format('FULL_DATE')}

☑️ java.time.LocalDateTime

Methods for formatting java.time.LocalDateTime. We also have methods for converting a LocalDateTime into a ZoneDateTime.

Methods
  • format()
  • format(pattern)
  • format(pattern)
  • toZonedDateTime()
  • toZonedDateTime(zone)
Examples
${mylocaldatetime.format()}
${mylocaldatetime.format('yyyy-MM-dd HH : mm : ss')}
${mylocaldatetime.format('MEDIUM_DATETIME')}
${mylocaldatetime.toZonedDateTime()}
${mylocaldatetime.toZonedDateTime('Asia/Seoul')}
${mylocaldatetime.toZonedDateTime('Asia/Seoul')}.format('yyyy-MM-dd HH : mm : ss Z')}

When no pattern is specified, ISO_LOCAL_DATE_TIME is used.

When no zone are given to toZoneDateTime, the ZoneStrategy are used to pick one for you.

☑️ java.time.LocalTime

Methods for formatting java.time.LocalTime.

Methods
  • format()
  • format(pattern)
Examples
${mylocaltime.format()}
${mylocaltime.format('HH : mm : ss')}
${mylocaltime.format('SHORT_TIME')}

When no pattern is specified, ISO_LOCAL_TIME is used.

☑️ java.time.MonthDay

Methods for formatting java.time.MonthDay.

Methods
  • format()
  • format(pattern)
Examples
${mymonthday.format()}
${mymonthday.format('MM dd')}

When no pattern is specified, MM:dd is used.

☑️ java.time.OffsetDateTime

Methods for formatting java.time.OffsetDateTime.

Methods
  • format()
  • format(pattern)
  • format(pattern, zone)

When no pattern is specified, ISO_OFFSET_DATE_TIME is used.

Uses the normalized ZoneId from the Offset myOffsetDateTime.getOffset().normalized()

Examples
${myoffsetdatetime.format()}
${myoffsetdatetime.format('yyyy MM dd HH mm ss')}
${myoffsetdatetime.format('FULL_DATETIME')}
${myoffsetdatetime.format('yyyy MM dd HH mm ss Z', 'Asia/Seoul')}

☑️ java.time.OffsetTime

Methods for formatting java.time.OffsetTime.

Methods
  • format()
  • format(pattern)
Examples
${myoffsettime.format()}
${myoffsettime.format('HH mm ss')}
${myoffsettime.format('MEDIUM_TIME')}

When no pattern is specified, ISO_OFFSET_TIME is used.

☑️ java.time.Period

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

Methods
  • days()
  • months()
  • years()
Examples
${myperiod.days}
${myperiod.months}
${myperiod.years}

☑️ java.time.Year

Methods for formatting java.time.Year.

Methods
  • format()
  • format(pattern)
Examples
${myyear.format()}
${myyear.format('yyyy')}

When no pattern is specified, yyyy is used.

☑️ java.time.YearMonth

Methods for formatting java.time.YearMonth.

Methods
  • format()
  • format(pattern)
Examples
${myyear.format()}
${myyear.format('yyyy MM')}

When no pattern is specified, yyyy-MM is used.

☑️ java.time.ZonedDateTime

Methods for formatting java.time.ZonedDateTime.

Methods
  • format()
  • format(pattern)
  • format(pattern, zone)
Examples
${myzoneddatetime.format()}
${myzoneddatetime.format('yyyy-MM-dd HH mm s Z')}
${myzoneddatetime.format('yyyy-MM-dd HH mm s Z', 'Asia/Seoul')}

Example:

new Java8ObjectWrapper(VERSION_2_3_33, new EnvironmentZoneStrategy());
// or
new Java8ObjectWrapper(VERSION_2_3_33, new StaticZoneStrategy(ZoneId.of("Europe/Oslo")));

☑️ java.time.ZonedId

Methods for formatting ZoneId display name.

You can override the textstyle with one of these values:

  • FULL
  • FULL_STANDALONE
  • SHORT
  • SHORT_STANDALONE
  • NARROW
  • NARROW_STANDALONE

You can also override the locale, but Java only seems to have locale support for a few languages.

Methods
  • format()
  • format(textStyle)
  • format(textstyle, locale)

Example

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

☑️ java.time.ZonedOffset

Methods for formatting the ZoneOffset display name. You can override the textstyle with one of these values:

  • FULL
  • FULL_STANDALONE
  • SHORT
  • SHORT_STANDALONE
  • NARROW
  • NARROW_STANDALONE

You can also override the locale, but Java only seems to have locale support for a few languages.

Methods
  • format()
  • format(textStyle)
Examples
${myzoneoffset.format()}
${myzoneoffset.format('short')}

Comparison

☑️ java.time.LocalDate

Can compare two LocalDate objects for equality.

Methods
  • isEqual(localDate)
  • isAfter(localDate)
  • isBefore(localDate)
Examples
${localDate.isEqual(anotherlocalDate)}
${localDate.isAfter(anotherlocalDate)}
${localDate.isBefore(anotherlocalDate)}

☑️ java.time.LocalDateTime

Can compare two LocalDateTime objects for equality.

Methods
  • isEqual(localDateTime)
  • isAfter(localDateTime)
  • isBefore(localDateTime)
Examples
${localDateTime.isEqual(anotherlocalDateTime)}
${localDateTime.isAfter(anotherlocalDateTime)}
${localDateTime.isBefore(anotherlocalDateTime)}

☑️ java.time.LocalTime

Can compare two LocalTime objects for equality.

Methods
  • isEqual(localDateTime)
  • isAfter(localDateTime)
  • isBefore(localDateTime)
Examples
${localTime.isEqual(anotherlocalTime)}
${localTime.isAfter(anotherlocalTime)}
${localTime.isBefore(anotherlocalTime)}

Manipulating time

☑️ 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
  • plusSeconds(long)
  • plusMinutes(long)
  • plusDays(long)
  • plusWeeks(long)
  • plusMonths(long)
  • plusYears(lone)
Examples
${localDateTime.plusMonths(1).plus.Hours(-2).plusMinutes(5).plusSeconds(30).format()}