❝Bands don’t play the whole LP. They play a selection of the songs that they like.❞
Peter Hook
An important component of the FreshMarker template engine is the built-in expression language. The expression language is used to utilize not only variables but also complex expressions in directives and interpolations. When evaluating other expression languages for customer projects, sometimes I am interested in integrating some findings into the FreshMarker expression language.
The Spring Expression Language (SpEL) includes the Collection Selection feature. This feature can be used to generate new lists from existing lists. The expression members.?[nationality == 'Serbian'] for example generates a new list from the list members that only contains the elements for which the attribute nationality has the value Serbian. In addition to the operator .?, there are two other variants, .^ and .$, which do not return a list, but rather the first or last element of the new list.
A new Select Operator is not absolutely necessary for FreshMarker because the existing options of the template engine are sufficient. To filter lists, the List Directive can be combined with If Directives or the filter attribute of the List Directive can be used. Even if some enhancements are not necessary, they show ways for further development.
The FreshMarker expression language has a Dynamic Key Operator, whose expression is specified inside square brackets. Depending on the expression, this operator has completely different semantics. If the expression returns an Integer value, it is the index in a Sequence or a String. If the expression returns a Range, it describes a slice operator. If the expression returns a String value, it is the name of an attribute to be accessed.
The new Select Operator can be inserted into the semantics of the Dynamic Key Operator without affecting the other interpretations. A Dynamic Key Operator behaves like a Select Operator if the expression can be evaluated to a Boolean value. For each element of the list, if the expression evaluates to true, the element also becomes an element of the new list; if the expression evaluates to false, the element does not become an element of the new list.
In FreshMarker, the SpEL example uses the slightly different syntax members[this.nationality = 'Serbian']. The .? operator is omitted because we use the Dynamic Key Operator with its square brackets. Within the expression, the attribute is accessed via the variable this. This makes the attribute’s affiliation clearer and also has a technical background. The operators .^ and .$ are omitted because they can be replaced in the new list using the built-ins ?first and ?last.
The implementation for the Select Operator starts in the InterpolationBuilder, which creates the Template instance from the parse tree. Depending on the type of dynamicKey, the current implementation creates either a TemplateSlice or a TemplateDynamicKey instance.
@Override
public TemplateObject visit(DynamicKey expression, Object input) {
TemplateObject dynamicKey = expression.get(1).accept(this, null);
if (dynamicKey instanceof TemplateRange templateRange) {
return new TemplateSlice(((TemplateObjectAndNode) input).templateObject(), templateRange);
}
return new TemplateDynamicKey(((TemplateObjectAndNode) input).templateObject(), dynamicKey);
}
TemplateSlice is a specialization when the dynamicKey is already recognized as a Range. Since FreshMarker types are often only recognized when evaluating variables, this is a helpful performance optimization.
@Override
public TemplateObject visit(DynamicKey expression, Object input) {
dictionary.push();
dictionary.putVariable("this", VariableType.THIS);
TemplateObject dynamicKey = expression.get(1).accept(this, null);
dictionary.poll();
TemplateObject templateObject = ((TemplateObjectAndNode) input).templateObject();
return switch (dynamicKey) {
case TemplateRange templateRange -> new TemplateSlice(templateObject, templateRange);
case TemplateBooleanExpression booleanExpression -> new TemplateSequenceFilter(templateObject, booleanExpression);
case null, default -> new TemplateDynamicKey(templateObject, dynamicKey);
};
}
The new implementation also considers TemplateBooleanExpression. These are predicates such as this < 42 or true. If a predicate is present, we know that a Select Operator should be used at this point and a TemplateSequenceFilter is created for this purpose.
To ensure that the this variable is recognized correctly, it is added to the dictionary as a variable of type VariableType.THIS. At this point, we still need to check how existing variables with the name this can be handled. Fortunately there is still some time until the next release.
Finally, the new list must be created in the TemplateSequenceFilter. To do this, the existing list is traversed and the predicate is called on each value in the list. To ensure that the variable this always points to the current value, a special ThisVariableEnvironment is inserted into the context and removed again at the end.
@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();
ThisVariableEnvironment variableEnvironment = new ThisVariableEnvironment(old, context);
context.setEnvironment(variableEnvironment);
try {
List<Object> filtered = new ArrayList<>();
for (Object object : sequence.sequence()) {
variableEnvironment.setObject(object);
if (predicate.evaluateToObject(context) == TemplateBoolean.TRUE) {
filtered.add(object);
}
}
return new TemplateListSequence(filtered);
} finally {
context.setEnvironment(old);
}
}
Currently the evaluation only works in the TemplateSequenceFilter instance and not yet in the TemplateDynamicKey instance. This is due to the fact that the dynamicKey is evaluated first in order to decide which mechanism should be used. However, the Select Operator requires an entry from the list, and so far this overhead has been dispensed with. As a consequence the Select Operator must be a predicate containing a relational, equality, or negation operator as its root.
The Select Operator will be included in the next version of FreshMarker. Stay tuned!