FreshMarker User Directives (2)

Im ersten Teil zum Thema User Directives wurde erläutert, wie diese in die FreshMarker Engine eingefügt sind und wie eigene Java Directives erstellt werden können. In diesem Teil werden die User Directives um FreshMarker Macros erweitert.

Ein FreshMarker Macro ist eine User Directive, die mit den Sprachmitteln der Template Engine formuliert wurde. Das folgende Macro hat den Namen entry und einen Parameter count.

<#macro entry count>
<#list 1..count as v>
${v}. <#nested/>
</#list>
</#macro>

<@entry count=3>Dies ist ein Test</@entry>

Das Macro erzeugt eine nummerierte Liste mit ansonsten identischen Einträgen.

1. Dies ist ein Test
2. Dies ist ein Test
3. Dies ist ein Test

Innerhalb des Macros kann das Element <#nested/> verwendet werden, um den Inhalt der Directive @entry in die Ausgabe des Macros einzufügen. In diesem Beispiel also den Text Dies ist ein Test. Ohne das Element <#nested/> würde der Inhalt der Directive @entry ignoriert werden.

Damit die Template Engine mit der FreshMarker Macro umgehen kann, muss die folgende JavaCC 21 Regel ausgewertet werden.

MacroDefinition :
{
    Token t;
}
    (<FTL_DIRECTIVE_OPEN1>|<FTL_DIRECTIVE_OPEN2>)
    (
      t=<MACRO>
      |
      t=<FUNCTION>
    )
    <BLANK>
    IdentifierOrStringLiteral
    [<OPEN_PAREN>]
    [ParameterList]
    [<CLOSE_PAREN>]
    <CLOSE_TAG>
    Block
    DirectiveEnd(t.getImage())
;

Wenn das zweite Element dieser Regel MACRO ist, dann handelt es sich um eine Macro Direktive und alle weiteren notwendigen Elemente der Regel werden ausgewertet. Im folgenden ist die Visitor Methode für die MacroDefinition dargestellt.

@Override
public BlockFragment visit(MacroDefinition ftl, BlockFragment input) {
  TokenType type = ftl.getChild(1).getTokenType();
  if (type != TokenType.MACRO) {
    return input;
  }
  String name = getName(ftl.getChild(3));
  List<ParameterHolder> parameterList = getParameterHolders(ftl);
  Fragment block = getFragment(ftl);
  logger.debug("type={}, name={}, block={}", type, name, block);
  template.getUserDirectives().put(name, new MacroUserDirective(block, parameterList));
  return input;
}

Der Name der Macro Definition wird an Position drei ausgelesen. Da die Grammatik von FreshMarker auf der von FreeMarker basiert, ist an dieser Stelle kurioserweise auch ein String Literal gestattet. Danach werden die Parameter als Liste von ParameterHoldern und der Inhalt des Macros als Fragment ausgelesen. Existieren keine Parameter, dann ist die Liste leer und wenn das Macro keinen Inhalt besitzt, wird das leere ConstantFragment.EMPTY verwendet.

Die Parameter von FreshMarker Macros können einen Defaultwert besitzen, der verwendet wird, wenn der Parameter im Aufruf der User Directive nicht angegeben wird. Die ensprechende Regel in der Grammatik sieht wie folgt aus.

ParameterList :
    <IDENTIFIER>
    [(<EQUALS>Expression) | <ELLIPSIS>]
    (
       [<COMMA>]
       <IDENTIFIER>
       [(<EQUALS>Expression) | <ELLIPSIS>]
    )*
;

In FreeMarker müssen die Parameter darauf geprüft werden, ob die ELLIPSIS nur beim letzten Parameter deklariert wurde und nach den Parametern mit einem Default-Werte keine Parameter ohne Default-Werte auftreten. Die ist eine Konsequenz aus dem Positional Parameters Feature von FreeMarker, bei dem die Parameter ohne Namen aber in der korrekten Reihenfolge ihrer Definition verwendet werden können.

In FreshMarker existiert dieses Feature nicht, daher führt die Verwendung der ELLIPSIS zu einem Fehler und Default-Werte können an allen Parametern ohne Beachtung ihrer Position deklariert werden.

Da die Macros während der Generierung der Template Instanzen erzeugt werden und zu dem Zeitpunkt noch keine Environment existiert, können sie nur in der Configuration oder im Template gespeichert werden. Da sich ihre Verwendung nur auf die Template Instanz bezieht, werden sie dort gespeichert.

Bei der Prozessierung des Templates wird dann mit einem Decorator für das Environment der Zugriff auf die MacroUserDirective Instanzen in der Template Instanz realisiert.

public void process(Map<String, Object> dataModel, Writer writer) {
  ProcessContext context = configuration.createContext(dataModel, writer);
  context.setEnvironment(new WrapperEnvironment(context.getEnvironment()) {
    @Override
    public UserDirective getDirective(String name) {
      UserDirective userDirective = userDirectives.get(name);
      return userDirective != null ? userDirective : super.getDirective(name);
    }
  });
  rootFragment.process(context);
}

Als letztes fehlt nur noch die MacroUserDirective mit ihrer execute Methode und das NestedInstructionFragment und seine process Methode.

@Override
public void execute(ProcessContext context, Map<String, TemplateObject> args, BlockFragment body) {
  Map<String, TemplateObject> values = evaluateParameterValues(args);
  log.debug("macro parameter values: {}", values);
  context.setEnvironment(new WrapperEnvironment(context.getEnvironment()) {
    @Override
    public TemplateObject getValue(String name) {
      TemplateObject value = values.get(name);
      return value != null ? value : super.getValue(name);
    }

    @Override
    public Optional<Fragment> getNestedContent() {
      return Optional.ofNullable(body);
    }
  });
  block.process(context);
}

Auch die MacroUserDirective arbeitet mit einem Decorator für das Environment. Die Werte der Parameter aus der User Directive oder entsprechende Default-Werte werden dem Fragment block und seinen Unterfragmenten als Variablen vorgesetzt und verdecken dabei ggf. bestehende Variablen mit gleichen Namen.

Außerdem überschreibt der Decorator das Ergebnis der neuen Methode getNestedContent, in dem der Inhalt der User Directive als Fragment für das NestedInstructionFragment bereitgestellt wird. Dieses Fragment liest den Inhalt von getNestedContent und prozessiert diesen, wenn er existiert.

public class NestedInstructionFragment implements Fragment {

  @Override
  public void process(ProcessContext context) {
    context.getEnvironment().getNestedContent().ifPresent(n -> n.process(context));
  }
}

Damit sind einfach Macros für die Template-Engine FreshMarker implementiert. Es fehlen nur das <#return/> Element für ein frühzeitiges Verlassen des Macros und Nested Loop Variables, die vielleicht schon in einem der nächsten Beitrage umgesetzt werden.

Schreibe einen Kommentar