Select Operator for FreshMarker (2)

To improve is to change; to be perfect is to change often.

Winston Churchill

In the first article on the Select Operator, the implementation was provisionally terminated with a few weaknesses. In this article, two of the weaknesses are eliminated.

The Select Operator is a Dynamic Key Operator in which the expression used is a predicate that can access the attributes of the elements in a list. The following expression generates a list from the elements of an existing list whose nationality is Serbian.

members[this.nationality = 'Serbian']

The first vulnerability is the definition of a new reserved word this for the dynamic key operator. This poses the risk that existing templates will no longer work because they use a variable called this.

members[this]

This form can refer to any form of the Dynamic Key Operator. The problem can be solved by a lambda variant of the expression.

members[member -> member.nationality = 'Serbian']

With lambdas, the problem can be circumvented, as the variable this can be replaced with any name. But the grammar for FreshMarker does not currently include lambda expressions.

A second problem was that the Select Operator required a relational, equality, or negation operator as the root of the expression. This ensured that it was recognized as a predicate. The following Select Operator has not worked so far.

employees[this.name?ends_with('Kaiser')]

Here, the root of the expression is a built-in operator, and there is currently no information available about the return value of this built-in. In general, it is actually possible to determine whether a built-in is a predicate only by evaluating it.

Here, too, the use of lambdas can be a solution. The lambda simply determines whether the Dynamic Key Operator is a Select Operator.

In order to implement lambda expressions in FreshMarker, the CongoCC grammar for FreshMarker must first be extended.

LambdaLhs:
    (
        <IDENTIFIER>
    |
        <OPEN_PAREN>
        <IDENTIFIER>
        (
            <COMMA>
            <IDENTIFIER>
        )*
        <CLOSE_PAREN>
    )
;

LambdaExpression :
    LambdaLhs <LAMBDA> =>|| Expression
;

The LambdaLhs rule describes the left side of the lambda expression. This can be a single variable name or one or more comma-separated names in parentheses. The LamdaExpression itself consists of the LambdaLhs, the Lamda operator ->, and a subsequent expression. The LambdaExpression is inserted into the BaseExpression.

BaseExpression :
    LambdaExpression
    | <IDENTIFIER>
    | NumberLiteral
    | HashLiteral
    | <STRING_LITERAL>
    | <TRUE>
    | <FALSE>
    | <NULL>
    | ListLiteral
    | Parenthesis
    | BuiltinVariable
;

Now all that’s missing is the evaluation of the lambda expressions. Since only one variable is required so far, the visit method throws a ParsingException for other variants.

@Override
public TemplateObject visit(LambdaExpression expression, Object input) {
  if (!(expression.getFirst() instanceof IDENTIFIER)) {
    throw new ParsingException("only lambda with single variable allowed", expression);
  }
  String identifier = expression.getFirst().toString();
  dictionary.push();
  dictionary.putVariable(identifier, VariableType.VAR);
  TemplateObject lambdaExpression = expression.getLast().accept(this, null);
  dictionary.poll();
  return new TemplateLambda(identifier, lambdaExpression);
}

A variable is created in the dictionary for the recognized variable name, and the expression is evaluated with it. The evaluateToObject method in TemplateSequenceFilter changes slightly due to the TemplateLambda class.

@Override
public TemplateObject evaluateToObject(ProcessContext context) {
  TemplateObject templateObject = sequenceOrMap.evaluateToObject(context);
  if (templateObject == TemplateNull.NULL) {
    return TemplateNull.NULL;
  }
  if (!(templateObject instanceof TemplateSequence<?> sequence)) {
    throw new ProcessException("unsupported type: " + templateObject.getModelType());
  }

  Environment old = context.getEnvironment();
  VariableEnvironment variableEnvironment = new VariableEnvironment(old);
  context.setEnvironment(variableEnvironment);
  try  {
    List<Object> filtered = new ArrayList<>();
    for (Object object : sequence.sequence()) {
      variableEnvironment.createVariable(lambda.getVariable(), context.mapObject(object));
      if (lambda.evaluate(context, TemplateBoolean.class) == TemplateBoolean.TRUE) {
	filtered.add(object);
      }
    }
    return new TemplateListSequence(filtered);
  } finally {
    context.setEnvironment(old);
  }
}

Instead of the new ThisVariableEnvironment class used in the previous post, the VariableEnvironment used for FreshMarker variables is now used here. The current element from the list is stored under the variable name from the lambda and the lambda expression is evaluated with it.

With these changes, the Select Operator for ranges and sequences is now complete, and further additions can be made in the near future.

Leave a Comment