Die FreshMarker Grammatik

Im ersten Beitrag wurde aus der Vogelperspektive beschrieben, wie die FreshMarker Template-Engine zu bedienen ist. Ausgelassen wurde dabei, wie aus der Textdarstellung eines Templates eine Instanz der Klasse Template generiert wird.

Um ein Template syntaktisch korrekt und sauber zu verarbeiten, bietet es sich an, einen passenden Parser zu verwenden. Mit dem Parser-Generator JavaCC 21 von Jonathan Revusky steht das passende Werkzeug zum Erzeugen eines eigenen Parsers zur Verfügung. JavaCC 21 liefert erfreulicherweise auch eine eigene FreeMarker Grammatik, mit der die eigene Entwicklung zügig beginnen kann.

Um den Parser bei Änderungen an der Grammatik automatisch zu generieren, wurde ein passendes Plugin in den Maven Build aufgenommen.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.0.0</version>
  <executions>
    <execution>
      <phase>generate-resources</phase>
      <goals>
        <goal>exec</goal>
      </goals>
      <configuration>
        <executable>${javacc}</executable>
        <arguments>
          <argument>-classpath</argument>
          <classpath/>
          <mainClass>com.javacc.Main</mainClass>
          <argument>-jdk11</argument>
          <argument>-d</argument>
          <argument>${project.build.directory}/generated-sources</argument>
          <argument>src/main/resources/javacc/FTL.javacc</argument>
        </arguments>
      </configuration>
    </execution>
  </executions>
</plugin>

Das Plugin erzeugt, mit Hilfe des JavaCC Parser-Generators, einen Parser für FreeMarker Templates. Dieser Parser wird in den folgenden getTemplate Methoden genutzt, um einen Parse-Tree aus der Textdarstellung des Templates zu erzeugen.

public Template getTemplate(String name) throws IOException, ParseException {
  return getTemplate(name, StandardCharsets.UTF_8);
}

public Template getTemplate(String name, Charset charset) throws ParseException, IOException {
  try (Reader reader = templateLoader.getTemplate(name).map(t -> t.getReader(charset))
      .orElseThrow(() -> new TemplateNotFoundException("template not found: " + name))) {
    FTLParser parser = new FTLParser(reader);
    parser.Root();
    Root root = (Root)parser.rootNode();
    Template template = new Template(this);
    root.accept(new FragmentBuilder(), template.getRootFragment());
    return template;
  }
}

Der FTLParser wird mit einem Reader initialisiert, den der TemplateLoader bereitstellt. Nach dem Aufruf der Methode Root() steht der Parser-Tree zur Verfügung und kann zur Weiterverarbeitung genutzt werden. Tritt beim Parsen der Eingabe ein Fehler auf, dann wird Methode mit einer ParseException verlassen.

Der Parse-Tree kann auf verschiedene Arten ausgewertet werden. Die JavaCC 21 Grammatik kann mit speziellen Code für alle notwendigen Regeln ausgestattet werden, damit beim Erzeugen des Parse-Tree die Template-Konstrukte generiert werden. Der vollständige Parse-Tree kann aber auch im Nachgang ausgewertet werden. Für FreshMarker wurde der zweite Weg gewählt, damit die Eingriffe in die Grammatik minimal bleiben und neue Versionen einfacher übernommen werden können. Außerdem fehlt es auch etwas an Expertise für JavaCC 21 Grammatiken.

Ein elegantes Design Pattern, um Arbeiten an komplexen Bäume zu implementieren, ist das Visitor Pattern. Dieses Pattern wurde schon in einer ganzen Reihe von Beiträgen vorgestellt und benötigt für unsere ersten Templates die folgende FragmentBuilder Implementierung.

public class FragmentBuilder implements FtlVisitor<BlockFragment, BlockFragment> {

  @Override
  public BlockFragment visit(Root ftl, BlockFragment input) {
    for (Node node : ftl.children(true)) {
      node.accept(this, input);
    }
    return input;
  }

  @Override
  public BlockFragment visit(Block ftl, BlockFragment input) {
    for (Node node : ftl.children(true)) {
      node.accept(this, input);
    }
    return input;
  }

  @Override
  public BlockFragment visit(Text ftl, BlockFragment input) {
    ftl.getAllTokens(false).stream().map(Token::getImage).map(TemplateString::new).map(ConstantFragment::new)
        .forEach(input::addFragment);
    return input;
  }
}

Der FramentBuilder besitzt für jedes verwendete Element der FreeMarker Grammatik eine visit Methode. Im Fall von Root und Block werden die accept Methoden ihrer Kind-Elemente aufgerufen und im Fall des Text Elementes werden für alle Kind-Elemente entsprechende ConstantFragment Instanzen erzeugt und in das übergebene BlockFragement eingefügt. Die accept Methoden aller Grammatik Elemente sehen identisch aus.

public <I, O> O accept(FtlVisitor visitor, I input) {
  return (O)visitor.visit(this, input);
}

Es werden ein Visitor vom Typ FtlVisitor und ein Eingabeparameter vom Typ I übergeben und das Element ruft die für seinen Typ vorgesehene visit Methode auf, die ein Ergebnis vom Typ O zurück liefert. Das mutet etwas kompliziert an, hilft aber ungemein, wenn in einem Parse-Tree nicht klar ist, welche Kind-Elemente vom welchen Typ unter dem aktuellen Element hängen. Statt mit viel eigenem Code herauszufinden, welcher Element-Typ behandelt werden muss, lässt man dies von den Elementen selbst entscheiden.

Die accept Methoden müssen in die Klassen der Grammatik Elemente eingefügt werden. Das klingt zunächst kompliziert, ist aber durch den INJECT Mechanismus von JavaCC 21 trivial.

INJECT Root :
  import org.freshmarker.core.ftl.FtlVisitor;
{
   public <I, O> O accept(FtlVisitor visitor, I input) {
      return (O)visitor.visit(this, input);
   }
}

Die obige Anweisung am Ende der FTL Grammatik fügt die entsprechende Methode und den notwendigen Import bei der Parser-Generierung automatisch in die Klasse Root ein. Eine entsprechende Anweisung wird für alle Elemente der Grammatik benötigt, die tatsächlich genutzt werden. Da hier inkrementell entwickelt wird, reicht es aus, immer nur die Elemente hinzuzufügen, die zusätzlich unterstützt werden sollen.

root.accept(new FragmentBuilder(), template.getRootFragment());

Gestartet wird die Bearbeitung, in dem die accept Methode des Wurzelknoten aufgerufen wird und als Parameter der FragmentBuilder und das Wurzelfragment des Templates übergeben werden. Wenn kein Fehler aufgetreten ist, dann befinden sich nach diesem Aufruf alle für das Template notwendigen Fragmente unterhalb der Wurzelfragments.

Die bisherige Implementierung beherrscht nur statische Inhalte. Damit sie dies ändert wir im nächsten Beitrag die Implementierung der Variableninterpolation betrachtet.

Schreibe einen Kommentar