Mit der Variableninterpolation erhielten die FreshMarker Templates einen ersten Schub von Dynamik. Durch Direktiven wird es darüber hinaus möglich, ganze Abschnitte eines Templates ein- und auszublenden oder mehrfach zu wiederholen.
Die beiden bekanntesten Strukturen für solche Direktiven sollen hier mit ihrer Implementierung für FreshMarker vorgestellt werden.
Die If-ElseIf-Else Direktive blendet einen von mehreren Blöcken, abhängig von einem boolschen Ausdruck, ein.
<#if x == 1> x is 1 <#elseif x == 2> x is 2 <#elseif x == 3> x is 3 <#else> x is not 1 nor 2 nor 3 </#if>
In diesem Beispiel wird, abhängig vom Wert der Variablen x einer der vier Blöcke dargestellt. Passt keiner der Ausdrücke der if
oder elseif
Blöcke, dann wird der else
Block gewählt.
@Override public BlockFragment visit(IfStatement ftl, BlockFragment input) { IfFragment ifFragment = ftl.accept(new IfFragmentBuilder(), null); input.addFragment(ifFragment); return input; }
Damit die If-ElseIf-Else Direktive erkannt wird, muss der FragmentBuilder
um eine entsprechende visit
Methode erweitert werden. Diese Methode delegiert die Erzeugung einer IfFragment
Instanz an einen spezialisierten Visitor
vom Typ IfFragmentBuilder
.
In der Grammatik existiert für das IfStatement
die folgende Regel. Interessant für die Auswertung sind hier die Expression
an Position vier, der Block
an Position sechs, sowie die danach folgenden ElseIfBlock
und Else
Block Elemente.
If #IfStatement : (<FTL_DIRECTIVE_OPEN1>|<FTL_DIRECTIVE_OPEN2>) <IF><BLANK> Expression <CLOSE_TAG> Block ( ElseIf )* [ Else ] DirectiveEnd("if") ;
Der IfFragmentBuilder
liest in der ersten visit
Methode diese Werte aus und erstellt passende Objekte.
public IfFragment visit(IfStatement ftl, IfFragment input) { IfFragment ifFragment = new IfFragment(); Node expression = ftl.getChild(3); TemplateObject ifExpression = expression.accept(interpolationBuilder, null); Node block = ftl.getChild(5); BlockFragment ifBlock = block.accept(new FragmentBuilder(), new BlockFragment()); ConditionalFragment ifPart = new ConditionalFragment(ifExpression, ifBlock); ifFragment.addFragment(ifPart); List<ElseIfBlock> elseIfParts = ftl.childrenOfType(ElseIfBlock.class); elseIfParts.forEach(elseIfPart -> elseIfPart.accept(this, ifFragment)); ElseBlock elsePart = ftl.firstChildOfType(ElseBlock.class); if (elsePart != null) { elsePart.accept(this, ifFragment); } return ifFragment; }
Aus der Expression
wird ein TemplateObject
und aus dem Block
ein BlockFragment
. Beide werden in ein ConditionalFragment
eingefügt, das Bestandteil des des IfFragments ist. Danach werden die optionalen ElseIfBlock Elemente ausgewertet und als weitere ConditionalFragment
Instanzen in das IfFragment eingefügt. Existiert auch ein ElseBlock
, dann wird dieses als ConditionalFragment
mit der Expression BooleanTemplate.TRUE
als letztes Fragment eingefügt.
Wird das Template verarbeitet, dann wird bei der Auswertung der IfFragment
Instanz die Liste seiner ConditionalFragment
Instanzen durchlaufen und die erste Instanz verarbeitet , deren Ausdruck BooleanTemplate.TRUE
ergibt.
public void process(Environment environment, Writer writer) { fragments.stream().filter(f -> f.getConditional().evaluateToObject(environment) == TemplateBoolean.TRUE) .findFirst().ifPresent(f -> f.process(environment, writer)); }
Damit ist der Kern der Implementierung der If-ElseIf-Else Direktive auch schon besprochen und die nächste etwas aufwendigere Direktive List rückt ins Rampenlicht.
Die hier vorgestellte Variante ist eine vereinfachte Syntax von der List Direktive von FreeMarker. Dies liegt aber größtenteils an der FTL Grammatik die mit JavaCC 21 mitgeliefert wird. Hier fehlen einfach die erweiterten Möglichkeiten und müssen wohl oder übel demnächst im Projekt FreshMarker nachgebaut werden.
<#list 1..4 as s> <#if s % 2 == 0> ${s} is even <#else> ${s} is odd </#if> </#list>
Im hier dargestellten Beispiel wird die Liste der Zahlen 1 bis 4 durchlaufen und abhängig vom Wert der aktuellen Zahl ausgegeben, ob diese gerade oder ungerade ist. Schon hier kann man gut erkennen, dass die If-Else-If Direktive weniger komplex ist als die List Direktive. Während Erstere nur anhand eines Ausdrucks ermittelt, ob ein Block dargestellt werden soll oder nicht, muss Letztere eine Laufvariable innerhalb des umschlossenen Bereich bereitstellen.
Das ListFragment
wird, dem IfFragment
recht ähnlich, durch den FragmentBuilder
erzeugt. In der Grammatik beinhaltet die ListInstruction
eine Expression
, einen IDENTIFIER
und einen Block
. Die Expression
muss bei der Verarbeitung des Template zu eine Sequenz ausgewertet werden über deren Elemente dann iteriert wird. Der IDENTIFIER
entspricht dem Namen der Laufvariable der ListFragment
Instanz. Bei jeder Iteration wird der Block
mit dem aktuellen Inhalt der Laufvariable ausgewertet.
List #ListInstruction : (<FTL_DIRECTIVE_OPEN1>|<FTL_DIRECTIVE_OPEN2>) <LIST><BLANK> Expression <AS> <IDENTIFIER> <CLOSE_TAG> Block DirectiveEnd("list") ;
Die Dynamik der ListFragment Intanz wird innerhalb der process
Methode realisiert. Zuerst wird die Sequenz und ihre Länge bestimmt und danach die Laufvariable vom Typ TemplateLooper
erzeugt.
public void process(Environment environment, Writer writer) { TemplateSequence sequence = (TemplateSequence) list.evaluateToObject(environment); int size = sequence.size(environment).asNumber().map(TemplateNumber::asInt) .orElseThrow(() -> new ProcessException("no number")); TemplateLooper looper = new TemplateLooper(sequence, size); ListEnvironent listEnvironment = new ListEnvironent(environment, identifier, looper); for (int i = 0; i < size; i++) { block.process(listEnvironment, writer); looper.increment(); } }
Innerhalb einer Schleife wird dann der Block prozessiert und danach die TemplateLooper
Instanz inkrementiert.
Damit die Laufvariable im Block
zur Verfügung steht wird sie über das Environment
bereitgestellt. Sie wird jedoch nicht in das aktuelle Environment
eingefügt, sondern über ein neues ListEnvironment
. Dieses ListEnvironment
delegiert alle Aufrufe zum übergeordneten Environment
. Ausgenommen davon ist einzig der Zugriff auf die Laufvariable.
public class ListEnvironent extends WrapperEnvironment { private final TemplateLooper looper; private final String identifier; public ListEnvironent(Environment wrapped, String identifier, TemplateLooper looper) { super(wrapped); this.identifier = identifier; this.looper = looper; } @Override public TemplateObject getValue(String name) { return identifier.equals(name) ? looper : wrapped.getValue(name); } }
Der Vorteil dieser Lösung ist, dass keine Manipulation des übergeordneten Environment
durch das ListFragment
gibt und so außerdem auch geschachtelte Listen mit dem selben Namen für die Laufvariable möglich sind.
@Test void generateMultiListDirectives() throws ParseException, IOException { templateLoader.putTemplate("test", "<#list sequence as s>" + "<#list 1..2 as s>" + "<#if s % 2 == 0>${s} is even<#else>${s} is odd</#if>" + "<#if s?has_next>, </#if>" + "</#list>" + "<#if s?is_last>.<#else>, </#if>" + "</#list>"); Template template = configuration.getTemplate("test"); Map<String, Object> dataModel = Map.of("sequence", List.of(1,2,3,4)); assertEquals("1 is odd, 2 is even, 1 is odd, 2 is even, 1 is odd, 2 is even, 1 is odd, 2 is even.", template.process(dataModel)); }
In diesem Beispiel werden zwei Listen mit gleichnamigen Laufvariablen durchlaufen. In den Zeilen sechs und sieben wird auf die innere Laufvariable und in der Zeile neun auf die äußere Laufvariable zugegriffen. In diesem Beispiel sind außerdem zwei Built-Ins für Laufvariablen zu sehen. Neben has_next
und is_last
, stehen noch die Built-Ins is_first
, index
, counter
, item_parity
, item_parity_cap
und item_cycle
bereit.
Wer die Entwicklung des Projekt FreshMarker verfolgen möchte kann den aktuellsten Stand auf GitLab finden.