The FreshMarker template engine has the Partial Template Reduction feature. This makes it possible to create a new Template
from an existing Template
by adding an incomplete model. Some constructs are simplified in the new Template
because the data from the incomplete model could be evaluated there. Until now, this could only be used in directives and interpolations, so why not use the mechanism in expressions as well?
It is not entirely correct that the mechanism has not yet been used in expressions. However, the all or nothing law has applied here up to now. If an expression could be evaluated, then its result was used in a directive or interpolation for simplification.
<#if price * amount < free_limit> Limit not reached <#else> Limit reached </#if>
In this simple example, the If Directive can be simplified if all three variables price
, amount
and free_limit
are known. In this case, the Template
is simplified by only containing the text Limit not reached
or Limit reached
. If one of the variables is not known, the If Directive is not yet simplified.
However, it would be nice if there was also a simplification if, for example, only price
and amount
are known.
<#if 42 < free_limit> Limit not reached <#else> Limit reached </#if>
In this example, the values 6 and 7 were present in the model and a simplification to the expression 42 < free_limit
was possible.
To make the simplifications, the implementations of TemplateObject
all receive a reduce
method. The contract for this method stipulates that it either returns a simplified form of the sub-expression that begins with this object or the object itself.
public interface TemplateObject { default TemplateObject reduce(ReduceContext context) { return this; } }
The method is defined as a default method in the TemplateObject
interface and therefore does not have to be implemented for all primitive model types. The simplification of an instance of TemplateString
is the instance itself and the simplification of a TemplateNumber
instance is the instance itself.
To gain initial experience with the new feature, we add it to the interpolation. To do this, we extend the reduce
method of the InterpolationFragment
class.
@Override public Fragment reduce(ReduceContext context) { TemplateMarkup reduced = partialExpressionReduction ? expression.reduce(context) : expression; try { TemplateString templateObject = reduced.evaluate(context, TemplateString.class); context.getStatus().replaced().incrementAndGet(); return new ConstantFragment(templateObject.getValue()); } catch (WrongTypeException e) { throw new ReduceException(e.getMessage(), ftl, e); } catch (ProcessException e) { if (partialExpressionReduction && reduced != expression) { context.getStatus().expressions().add(expression); context.getStatus().expressions().add(reduced); log.debug("Reduced: {} to {}", expression, reduced); return new InterpolationFragment(reduced, ftl, true); } return this; } }
In the first line, a simplified instance of the expression
is determined and then checked to see whether the InterpolationFragment
instance can be replaced by a ConstantFragment
instance. This part of the implementation has remained almost the same. If a ProcessException
is thrown in the reduce
method, then the InterpolationFragment
instance is not simply returned, but a new one if necessary.
If the feature is activated and we have another instance of TemplateMarkup
in reduced
, then we create a new InterpolationFragment
instance with the simplified expression. In addition, we remember the expression before and after the simplification in the ReductionStatus
. This is the only way we can recognize changes within the template and check them in the unit tests.
The previous implementations do not yet provide any simplifications because all TemplateObject
implementations return this
as the result of the reduce
method. For most of the leaves in our expression trees the behavior is already suitable, now we have to adapt the most important reduce
methods from the root of the expression.
First the TemplateMarkup
class, which is always the root of the expression in InterpolationFragment
instances.
@Override public TemplateMarkup reduce(ReduceContext context) { TemplateObject templateObject = content.reduce(context); if (templateObject.isNull() || templateObject == content) { return this; } if (templateObject.isPrimitive()) { return new TemplateMarkup(getString(context, templateObject)); } return new TemplateMarkup(templateObject); }
The content
is reduced in the first line. If the result is NULL
or no simplification could be made, the current instance is returned without changes, otherwise it is checked whether it is a primitive model type. In this case, a String
is created and a new TemplateMarkup
instance is returned. This also means that a ConstantFragment
instance is created from the String
, which replaces the current InterpolationFragment
. In the other case, a new TemplateMarkup
instance is created with the simplified expression.
The ModelVariable
class provides the variables from the model and also requires its own reduce
implementation.
@Override public TemplateObject reduce(ReduceContext context) { TemplateObject value = evaluateToObject(context); if (value == TemplateNull.NULL) { return this; } context.getStatus().expression().incrementAndGet(); return value; }
If the desired variable is not contained in the model, the implementation returns its own instance; otherwise, it replaces its own instance with the value from the model.
The previous implementations all have something in common, only with them the feature cannot be implemented, because they either return an expression that has been completely reduced because only filled ModelVariable
instances or primitive model types were involved or an expression that has not been reduced because other participants have returned this
. These other participants are inner nodes of the expression tree such as TemplateOperation
, TemplateJunction
, TemplateRelation
or TemplateNot
.
This also means that all expressions can only be reduced if all inner nodes receive an appropriate implementation. As long as this has not been realized, the feature remains experimental.
Fortunately, some inner nodes have already been adapted. Here is the implementation for TemplateRelational
.
@Override public TemplateObject reduce(ReduceContext context) { TemplateObject leftObject = left.reduce(context); TemplateObject rightObject = right.reduce(context); if (leftObject instanceof TemplatePrimitive<?> leftPrimitive && rightObject instanceof TemplatePrimitive<?> rightPrimitive) { TemplatePrimitive<?> result = leftPrimitive.relational(type, rightPrimitive, context); context.getStatus().expression().addAndGet(2); return result; } return new TemplateRelational(type, leftObject, rightObject); }
TemplateRelational
implements the relational operators <, ≤, ≥, > and <=>. To reduce such an expression, the two operands are reduced first. If both operands are primitive model types, the result of the relation is calculated and returned as a replacement. In the other case, there is a new TemplateRelational
instance with the reduced operands.
This feature will be part of the next Freshmarker version 2.2.0 and will gradually be extended to all directives.