„Gäbe es die letzte Minute nicht,
Mark Twain
so würde niemals etwas fertig.“
Einer der Gründe für die Entwicklung der FreshMarker Bibliothek, war die fehlende Unterstützung der Java Time API in FreeMarker. Daher existierte schon in der ersten Implementierung neben der Unterstützung für java.util.Date
, java.sql.Date
und java.sql.Time
auch eine Unterstützung für java.time.LocalDateTime
, java.time.LocalDate
und java.time.LocalTime
.
In diesem Beitrag wird die Formatierung für alle temporalen Typen verbessert. Bislang beherrschte FreshMarker nur die Standarddarstellung und eine Computer Audience Darstellung.
${dateTime}, ${dateTime?c} ${date}, ${date?c} ${time}, ${time?c}
Die oben dargestellten Interpolationen erzeugen die folgende Ausgabe.
2023-05-06 13:42:00, 2023-05-06T13:42:00 2023-05-06, 2023-05-06 13:42:00, 13:42:00
Die erste Darstellung ist jeweils für natürliche Leser und die zweite für technische Systeme. Bislang konnte dies nur über ein selbstgeschriebenes Plugin verändert werden, dass die Standardformatierung überschrieb. Das ist unelegant und nicht besonders flexibel. Aus diesem Grund werden nun spezielle Built-Ins hinzugefügt und die Setting Direktive erweitert.
Die neuen Built-Ins lehnen sich lose an ihre Namensvettern aus FreeMarker an. Sie haben den Bezeichner string
und besitzen einen Parameter, mit dem der Format-String übergeben wird.
${dateTime?string('d. MMMM yyyy hh:mm')}
Das Ergebnis der Interpolation ist der Text 6. Mai 2023 13:42
.
Die neuen Built-Ins ergänzen den TemporalPluginProvider
für die Typen LocalDateTime
, LocalDate
und LocalTime
. Andere Typen werden vorerst einmal nicht unterstützt. Der Kern der Implementierung ist die formatTemporal
Methode.
builtIns.put(DATE_TIME_BUILDER.of("string"), new FunctionalBuiltIn( (TemplateObject x, List<TemplateObject> y, ProcessContext e) -> formatTemporal(y, e, ((TemplateLocalDateTime) x).getValue())));
Der zu formatierende Wert wird als Temporal
Instanz an die Methode übergeben. Außerdem erhält die Methode die Parameter des Built-Ins als Liste übergeben. Die Liste muss genau ein Element enthalten, dass zum Typ TemplateString
ausgewertet wird. Der Formatierungsstring kann also nicht nur als Konstante übergeben werden, sondern auch aus einer Variable gelesen werden.
private static TemplateString formatTemporal(List<TemplateObject> y, ProcessContext e, Temporal value) { if (y.isEmpty()) { throw new ProcessException("missing format parameter"); } String pattern = y.get(0).evaluateToObject(e).asString().map(TemplateString::getValue).orElseThrow(() -> new ProcessException("invalid format parameter")); return new TemplateString(java.time.format.DateTimeFormatter.ofPattern(pattern, e.getEnvironment().getLocale()).format(value)); }
Neben den Built-Ins um einzelne Interpolations anzupassen, wird auch die Setting Direktive angepasst. Sie verändert alle Formatierungen in ihrem Geltungsbereich. Der Geltungsbereich der Setting Direktive ist das aktuelle Environment. Das BaseEnvironment
gilt für das gesamte Template
. Andere Environment
Implementierungen werden auf diesem gestapelt und schränken den Geltungsbereich ggf. weiter ein. Der Geltungsbereich des SettingEnvironment
ist so groß wie das übergeordnete Environment
, bei dem ListEnvironment
ist es nur der innere Teil einer Schleife.
Im nachfolgenden Beispiel ist die Verwendung der Setting Direktive dargestellt. Die erste Interpolation wird mit dem Standardformat ausgewertet und die zweite wird mit dem Format aus der Direktive ausgewertet.
${date} <#setting date_format="dd. MMMM yyyy"> ${date}
Die Setting Direktive wird im SettingFragment
verarbeitet. Innerhalb der process
Methode wird eine SettingEnvironment
erzeugt und als aktuelles Environment
in den Context
eingefügt.
Je nach Attributnamen, werden für unterschiedliche temporale Typen Formatter
erzeugt und in das SettingEnvironment
eingefügt. Die Implementierung ist nicht besonders komplex, daher werden in diesem Fall auch die älteren temporalen Typen unterstützt. Nachfolgende Setting
Direktiven werden auf dieser gestapelt und überschreiben dann ggf. Formatter
des gleichen Typs.
if ("date_format".equals(name)) { String value = setting.evaluate(context, TemplateString.class).getValue(); Map<Class<? extends TemplateObject>, Formatter> formatter = Map.of( TemplateLocalDate.class, new DateFormatter(value), TemplateClassicDate.class, new ClassicDateFormatter(value)); context.setEnvironment(new SettingEnvironment(context.getEnvironment(), null, null, formatter)); return; }
public class SettingEnvironment extends WrapperEnvironment { // ... private final Map<Class<? extends TemplateObject>, Formatter> formatters; public SettingEnvironment(Environment wrapped, Locale locale, OutputFormat format, Map<Class<? extends TemplateObject>, Formatter> formatters) { super(wrapped); this.locale = locale; this.format = format; this.formatters = formatters; } // ... @Override public <T extends TemplateObject> Formatter getFormatter(Class<T> type) { Formatter formatter = formatters == null ? null : formatters.get(type); return formatter != null ? formatter : wrapped.getFormatter(type); } }
Wird ein Formatter
benötigt, dann liefert die Methode getFormatter
die notwendige Instanz. Ist im aktuellen Environment kein passender Formatter
zu finden, dann wird im übergeordneten Environment wrapped
danach gesucht.
Die Anpassungen aus diesem Beitrag sind in der neuesten Version von FreshMarker auf Maven Central zu finden.
<dependency> <groupId>de.schegge</groupId> <artifactId>freshmarker</artifactId> <version>0.3.0</version> </dependency>