FreshMarker Variableninterpolation

Die FreshMarker Template-Engine, kann bislang nur konstante Inhalte ausgeben. Damit die Template-Engine nützlich wird, müssen variable Inhalte möglich werden. Ausdrücke der Form ${expression} werden von der FreshMarker Grammatik als Variableninterplolationen erkannt und durch das Ergebnis des Ausdrucks expression ersetzt.

Für die erste Umsetzung sind die folgenden Regeln der Grammatik beachtet worden.

Block : (SCAN 2 (Text | Interpolation))*;

Interpolation : <INTERPOLATE>Expression<CLOSE_BRACE>;

PrimaryExpression : BaseExpression (BuiltIn | Exists)*;

BaseExpression : <IDENTIFIER> | NumberLiteral | StringLiteral | BooleanLiteral | NullLiteral | Parenthesis;

BuiltIn : <BUILT_IN><IDENTIFIER>[<OPEN_PAREN>[ArgsList]<CLOSE_PAREN>];

Exists : <EXISTS_OPERATOR>;

ArgsList #void : (LOOKAHEAD(<IDENTIFIER><EQUALS>) NamedArgsList | PositionalArgsList);

PositionalArgsList : Expression ([<COMMA>]Expression)*;

Ein Block enthält neben Text auch Interpolation Elemente. Die wiederum beginnen mit dem INTERPOLATE Token (${) beinhalten ein Expression Element und enden mit }. Zu Anfang beschränkt sich die Expression in der Variableninterpolation auf PrimaryExpression, die aus einem Variablennamen IDENTIFIER oder einem Literal eines Basistyps und optionalen BuildIn oder Exists Elementen besteht.

Bei BuildIn Elementen handelt es sich um Funktionen, die auf diesen Basistypen ausgeführt werden können. Für den Typ String existieren beispielsweise die BuildIns upper_case und lower_case mit denen die String Werte in Groß- bzw. Kleinbuchstaben umgewandelt werden. Das Exists Element prüft ob eine Variable existiert und liefert in der Variableninterpolation true oder false zurück.

Damit die FreshMarker die Variableninterpolationen beachtet, muss der FragmentBuilder um eine entsprechende visit Methode ergänzt werden

@Override
public BlockFragment visit(Interpolation ftl, BlockFragment input) {
  logger.debug("interpolation: {}", ftl);
  TemplateObject interpolation = ftl.getChild(1).accept(interpolationBuilder, null);
  input.addFragment(new InterpolationFragment(interpolation));
  return input;
}

Diese Visitor Methode unterscheidet sich von den bisherigen FragmentBuilder Methoden darin, dass sie die Verarbeitung der Expression an einen anderen Visitor delegiert. Dieser InterpolationBuilder ist eine Implementierung des ExpressionVisitors, der nur auf Elementen der Grammatik arbeitet, die Bestandteil von FreshMarker Ausdrücken sind.

public class InterpolationBuilder implements ExpressionVisitor<Object, TemplateObject> {

  @Override
  public TemplateObject visit(PrimaryExpression expression, Object input) {
    TemplateObject base = children.get(0).accept(this, input);
    for (int i = 1; i < children.size(); i++) {
      base = children.get(i).accept(this, base);
    }
    return base;
  }

  @Override
  public TemplateObject visit(Token expression, Object input) {
  }

  @Override
  public TemplateObject visit(BuiltIn expression, Object input) {
  }
}

Die erste visit Methode delegiert die Verarbeitung des ersten Elementes an eine der anderen visit Methoden und in der darauffolgenden Schleife werden mögliche Exists und BuiltIn Elemente auf das bereits erzeugte Ergebnis angewendet.

public class InterpolationBuilder implements ExpressionVisitor<Object, TemplateObject> {

  @Override
  public TemplateObject visit(PrimaryExpression expression, Object input) {
  }

  @Override
  public TemplateObject visit(Token expression, Object input) {
    String image = expression.getImage();
    switch (expression.getType()) {
      case TRUE:
        return TemplateBoolean.TRUE;
      case FALSE:
        return TemplateBoolean.FALSE;
      case INTEGER:
        return new TemplateNumber(Integer.valueOf(image), Type.INTEGER);
      case DECIMAL:
        return new TemplateNumber(Double.valueOf(image), Type.DOUBLE);
      case STRING_LITERAL:
        return new TemplateString(image.substring(1, image.length() - 1));
      case RAW_STRING:
        return new TemplateString(image.substring(2, image.length() - 1));
      case IDENTIFIER:
        return new TemplateVariable(expression.getImage());
      case EXISTS_OPERATOR:
        return new TemplateExists((TemplateObject) input);
      default:
        throw new ProcessException("invalid token type: " + expression.getType());
    }
  }

  @Override
  public TemplateObject visit(BuiltIn expression, Object input) {
  }
}

Die zweite visit methode liefert für jedes unterstützte Token ein entsprechendes TemplateObjekt zurück. Für die Token-Typen TRUE, FALSE, INTEGER, DECIMAL, STRING_LITERAL und RAW_STRING die entsprechenden Basistypen und für IDENTIFIER und EXISTS_OPERATOR zwei spezielle TemplateObject Typen. Der Typ TemplateVariable bezieht bei der Templateprozessierung den tatsächlichen Wert anhand des übergebenen Namens aus dem aktuellen Environment. Der Typ TemplateExists überprüft das übergebene TemplateObject, also den Basisausdruck der Variableninterpolation, während der Templateprozessierung. Die Überprüfung liefert nur dann true zurück, wenn die Auswertung des TemplateObject nicht TemplateNull.NULL ergibt.

public class InterpolationBuilder implements ExpressionVisitor<Object, TemplateObject> {

  @Override
  public TemplateObject visit(PrimaryExpression expression, Object input) {
  }

  @Override
  public TemplateObject visit(Token expression, Object input) {
  }

  @Override
  public TemplateObject visit(BuiltIn expression, Object input) {
    Token buildInName = (Token) expression.getChild(1);
    List<TemplateObject> parameter = new ArrayList<>();
    if (expression.getChildCount() < 3) {
      return new TemplateFunction(buildInName.getImage(), (TemplateObject) input, List.of());
    }
    Node child = expression.getChild(3);
    if (child instanceof PositionalArgsList) {
      child.accept(new ParameterListBuilder(), parameter);
    } else {
      parameter.add(child.accept(this, null));
    }
    logger.info("parameters: {}", parameter);
    return new TemplateFunction(buildInName.getImage(), (TemplateObject) input, parameter);
  }
}

Die dritte visit Methode behandelt BuiltIn Elemente. Sie ist etwas komplexer, da ein BuiltIn auch Parameter besitzen kann. Im einfachen Fall wird eine TemplateFunction Instanz mit leerer Parameterliste erzeugt und im komplexeren Fall wird die Parameterliste mit Hilfe eines weiteren ExpressionVisitor vom Typ ParameterListBuilder erzeugt.

Die TemplateFunction ist, wie TemplateExists und TemplateVariable, ein spezielles TemplateObject, das während der Templateprozessierung dynamisch ausgewertet wird. Während die primitiven Typen wie TemplateBoolean, TemplateString und TemplateNumber statische Werte besitzen, können die Werte der anderen erst bestimmt werden, wenn das aktuelle Environment bereitsteht.

@AllArgsConstructor
public class TemplateFunction implements TemplateExpression {

  private final String name;
  private final TemplateObject expression;
  private final List<TemplateObject> parameter;

  @Override
  public TemplateObject evaluateToObject(Environment environment) {
    TemplateObject result = expression.evaluateToObject(environment);
    return environment.getBuiltIn(result.getClass(), name).apply(result, parameter, environment);
  }
}

Bei der Auswertung der TemplateFunction wird für dem ausgewerteten Ausdruck ein BuiltIn aus dem Environment geholt. Notwendig hierfür ist der Typ des ausgewerteten Ausdruck und der Name des BuiltIn. wenn ein entsprechendes BuiltIn existiert, dann wird mit dem Wert und den Parametern aufgerufen und sein Ergebnis zurück geliefert.

Wie die BuildIn im Environment bereitgestellt werden und wie sie funktionieren ist Inhalt des nächsten FreshMarker Beitrags.

Leave a Comment