FreshMarker Direktiven

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 ElseBlock 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.

Leave a Comment