Telephone Support für FreshMarker

Hat man eine Idee implementiert, dann ist die nächste oft nicht weit. Die Telephone Bibliothek kann nun schon mit Telefonnummern und Rufnummernblöcken umgehen und bietet Unterstützung für Bean Validation und Jackson. In diesem Beitrag kommt der Support der Template-Engine FreshMarker hinzu.

Die Integration von Erweiterungen ist ein wichtiges Feature der Template-Engine FreshMarker. Ein einfaches Plug-In System erlaubt es, eigene Datentypen und Funktionen hinzuzufügen. Zentrales Element ist dabei der eigene PluginProvider, der automatisch über den ServiceLoader geladen wird. Damit der PluginProvider der Telephone Bibliothek erkannt wird, wird eine Datei META-INF/services/org.freshmarker.core.plugin.PluginProvider benötigt, in der die zu ladene Klasse eingetragen wird.

Der PhoneNumberPluginProvider der Telephone Bibliothek registriert die zu unterstützenden Klassen InternationalPhoneNumber, NationalPhoneNumber und PhoneNumberBlock.

public class PhoneNumberPluginProvider implements PluginProvider {
    @Override
    public void registerMapper(Map<Class<?>, Function<Object, TemplateObject>> mapper) {
        mapper.put(InternationalPhoneNumber.class, o -> new TemplatePhoneNumber((PhoneNumber) o, true));
        mapper.put(NationalPhoneNumber.class, o -> new TemplatePhoneNumber((PhoneNumber) o, false));
        mapper.put(PhoneNumberBlock.class, o -> new TemplatePhoneNumberBlock((PhoneNumberBlock) o));
    }

    // ...
}

Für jede Klasse wird eine Funktion angegeben, wie deren Instanzen in TemplateObject Instanzen eingefügt werden. PhoneNumber Instanzen werden in TemplatePhoneNumber Instanzen eingefügt und PhoneNumberBlock Instanzen in TemplatePhoneNumberBlock Instanzen.

public class TemplatePhoneNumber extends TemplatePrimitive<PhoneNumber> {
    private final boolean international;
    public TemplatePhoneNumber(PhoneNumber phoneNumber, boolean international) {
        super(phoneNumber);
        this.international = international;
    }

    public PhoneNumber getNational() {
        return international ? ((InternationalPhoneNumber)getValue()).toNational() : getValue();
    }
}

Diese TemplateObject Subklassen sind nicht besonders komplex, weil alles Notwendige schon in der Klasse TemplatePrimitive implementiert wurde. Bei der Implementierung der FreshMarker Unterstützung fiel auf, dass die toString Methode der beiden PhoneNumber Klassen unterschiedlich implementiert wurden. Dies wurde bei der Gelegenheit korrigiert. Die Methode getNational wird für den nächsten Schritt bei der FreshMarker Unterstützung benötigt.

Wie das große Vorbild FreeMarker, kennt FreshMarker sogenannte Built-Ins. Diese Built-Ins sind Funktionen, die das Ergebnis einer Interpolation verändern. Die Interpolation "${firstName}" liefert beispielsweise den Text "Jens". Das Built-In upper_case verändert Text-Interpolationen, indem der Text in Großbuchstaben umgewandelt wird. Daher liefert die Interpolation "${firstName?upper_case}" den Text "JENS".

Für die TemplatePhoneNumber Klasse sollen die Bult-Ins national, ndc, sn und ext zur Verfügung gestellt werden. Für die TemplatePhoneNumberBlock Klasse gibt es die Built-Ins first, last und list.

public class PhoneNumberPluginProvider implements PluginProvider {
    private static final BuiltInKeyBuilder<TemplatePhoneNumber> PHONE_NUMBER_BUILDER = new BuiltInKeyBuilder<>(TemplatePhoneNumber.class);
    private static final BuiltInKeyBuilder<TemplatePhoneNumberBlock> PHONE_NUMBER_BLOCK_BUILDER = new BuiltInKeyBuilder<>(TemplatePhoneNumberBlock.class);

    @Override
    public void registerBuildIn(Map<BuiltInKey, BuiltIn> builtIns) {
        builtIns.put(PHONE_NUMBER_BUILDER.of("national"),
                new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext e) -> new TemplatePhoneNumber(
                        ((TemplatePhoneNumber) x).getNational(), false)));
        builtIns.put(PHONE_NUMBER_BUILDER.of("ndc"),
                new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext e) -> new TemplateString(
                        ((TemplatePhoneNumber) x).getValue().getNationalDestinationCode())));
        builtIns.put(PHONE_NUMBER_BUILDER.of("sn"),
                new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext e) -> new TemplateString(
                        ((TemplatePhoneNumber) x).getValue().getSubscriberNumber())));
        builtIns.put(PHONE_NUMBER_BUILDER.of("ext"),
                new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext e) -> new TemplateString(
                        ((TemplatePhoneNumber) x).getValue().getExtension().orElse(""))));
        builtIns.put(PHONE_NUMBER_BLOCK_BUILDER.of("first"),
                new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext e) -> ((TemplatePhoneNumberBlock) x).getFirst()));
        builtIns.put(PHONE_NUMBER_BLOCK_BUILDER.of("last"),
                new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext e) -> ((TemplatePhoneNumberBlock) x).getLast()));
        builtIns.put(PHONE_NUMBER_BLOCK_BUILDER.of("list"),
                new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext e) -> getSequence((TemplatePhoneNumberBlock) x)));
    }

    private static TemplateObject getSequence(TemplatePhoneNumberBlock x) {
        List<Object> templatePhoneNumbers = x.getValue().phoneNumbers().map(y -> new TemplatePhoneNumber(y, true)).map(Object.class::cast).toList();
        return new TemplateListSequence(templatePhoneNumbers);
    }
}

Die Built-Ins ndc, sn und ext liefern die einzelnen Komponenten der Telefonnummer als TemplateString Instanzen.

builtIns.put(PHONE_NUMBER_BUILDER.of("ndc"),
    new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext e) -> new TemplateString(((TemplatePhoneNumber) x).getValue().getNationalDestinationCode())));

Die Built-Ins werden, wie hier ndc, als FunctionalBuiltIn in eine Map eingefügt. Bei der Auswertung des Built-Ins wird die PhoneNumber aus der TemplatePhoneNumber Instanz geholt, der NationalDestinationCode extrahiert und als neue TemplateString Instanz zurückgegeben.

Das Built-In national liefert für eine internationale Telefonnummer die entsprechende nationale Variante und die Built-Ins first und last liefern für Rufnummernblöcke den ersten und letzen Eintrag als internationale Telefonnummer. Das Built-In list liefert alle Telefonnummern eines Rufnummernblocks als Liste zurück. Dazu nutzt es die neue Methode phoneNumbers der PhoneNumberBlock Klasse, die einen Stream von InternationalPhoneNumber Instanzen liefert.

Das folgende Template liefert für den Rufnummernblock eine HTML Liste mit Einträgen für die ersten 10 enthaltene Telefonnummer.

<ul>
<#list numberBlock?list[0..10] as s, l>
<li class="${l?item_parity}">${s}</li>
</#list>
</ul>

Für die Rufnummernblock "+49 173 1122 XXX" also die folgende Ausgabe.

<ul>
<li class="odd">+49 173 112200</li>
<li class="even">+49 173 112201</li>
<li class="odd">+49 173 112202</li>
<li class="even">+49 173 112203</li>
<li class="odd">+49 173 112204</li>
<li class="even">+49 173 112205</li>
<li class="odd">+49 173 112206</li>
<li class="even">+49 173 112207</li>
<li class="odd">+49 173 112208</li>
<li class="even">+49 173 112209</li>
</ul>

Damit ist die Unterstützung für die FreshMarker Template-Engine fertiggestellt und kann in eigenen Projekten genutzt werden.

Leave a Comment