At the end of the summer vacation, here is the last post on the topic of Partial Template Reduction for the time being. The basic algorithm was presented in the first two articles. In this article, the missing variable support is added.
The following example shows two directives that could not be reduced correctly until now.
<#var name=firstname> <#list seq as s with l> ${l?counter}. ${company}/${s} ${name} </#list>
The Var-Directive creates variables whose name
is known within a defined scope. Outside of this scope, there is no longer any access to the content of the variable. The List-Directive defines the variable s
and the loop variable l
, which can be accessed within the directive.
Without special handling in the reduce
method, all these variables can cause incorrect results. Within Partial Template Reduction, the variables of the List-Directive have no value and their interpolations are not replaced. If the variable of the Var-Directive has no value, it is not replaced either.
Variables and model data can overlap if they have the same name. With the previous reduce
implementation, however, it is not possible to determine whether one variable overlaps another because the scopes were ignored.
When processing a template with the process
method, validity ranges are created at necessary points using special Environment
implementations. These Environment
instances are chained and the search for a variable value is passed through from the topmost Environment
to the BaseEnvironment
. If the variable is found in one of the Environment
instances, the search is aborted and the value is returned. If the value is not found in the BaseEnvironment
, the search is terminated with an RuntimeException
.
There is a special ListEnvironment
for the List-Directive that contains the loop variables. When these variables are accessed within the loop, they can be found via the ListEnvironment
. If another loop with identical variables exists within the loop, only variables from the inner loop are found within this loop. This happens because the inner loops ListEnvironment
is searched first.
The Fragment
implementations for the List-Directive therefore also require an Environment
in their reduce
method to define the scope of their variables.
public class SequenceListFragment extends AbstractListFragment<Object> { // ... @Override public Fragment reduce(ReduceContext context) { Environment environment = context.getEnvironment(); try { context.setEnvironment(new ReducingVariableEnvironment(new ReducingLoopVariableEnvironment(environment, identifier, looperIdentifier))); return optimize(block, block.reduce(context), r -> new SequenceListFragment(list, identifier, looperIdentifier, r, ftl)); } finally { context.setEnvironment(environment); } } }
In the new reduce
method of the SequenceListFragment
, a new ReducingVariableEnvironment
and ReducingLoopVariableEnvironment
are inserted at the beginning of the environment chain and then the reduction is carried out.
public class ReducingLoopVariableEnvironment extends WrapperEnvironment { private final List<String> identifiers; public ReducingLoopVariableEnvironment(Environment wrapped, String... identifiers) { super(wrapped); this.identifiers = Arrays.stream(identifiers).filter(Objects::nonNull).toList(); } @Override public TemplateObject getValue(String name) { return identifiers.contains(name) ? TemplateNull.NULL : wrapped.getValue(name); } }
The ReducingLoopVariableEnvironment
is used because the loop variables never have a value during the reduction, only their existence is checked. If an outer variable with a value exists, it is successfully covered during the reduction and the value of the outer variable is not inadvertently inserted.
The reduction in the VariableFragment
is somewhat more complex because various cases have to be taken into account. First of all, the value of the variable can be NULL
during the reduction because it cannot be found in the model. In this case, the VariableFragment
remains unchanged and is returned directly. In the other case, the variable is created or changed in the Environment
and a new VariableFragment
is created
public class VariableFragment implements Fragment { // ... @Override public Fragment reduce(ReduceContext context) { Environment environment = context.getEnvironment(); try { TemplateObject value = expression.evaluateToObject(context); if (value.isNull()) { return this; } if (exists) { if (environment.getVariable(name) == null) { return this; } environment.setVariable(name, value); } else { if (environment.checkVariable(name)) { return this; } environment.createVariable(name, value); } context.getStatus().changed().incrementAndGet(); return new VariableFragment(name, value, exists, node); } catch (RuntimeException e) { return this; } } }
This is where an optimization of a TemplateObject
expression comes into play for the first time. Because if the value
of the variable has already been calculated, why shouldn’t it be used directly in the VariableFragment
?
For this to work, the ReducingVariableEnvironment
must be included in the reduce
method where a VariableEnvironment
is required in the process
method.
public class ReducingVariableEnvironment extends WrapperEnvironment { private final Set<String> identifiers = new HashSet<>(); public ReducingVariableEnvironment(Environment wrapped) { super(wrapped); } @Override public TemplateObject getValue(String name) { return identifiers.contains(name) ? TemplateNull.NULL : wrapped.getValue(name); } @Override public void createVariable(String name, TemplateObject value) { identifiers.add(name); } @Override public void setVariable(String name, TemplateObject value) { if (!identifiers.contains(name)) { wrapped.setVariable(name, value); return; } identifiers.add(name); } @Override public boolean checkVariable(String name) { return identifiers.contains(name); } @Override public TemplateObject getVariable(String name) { return identifiers.contains(name) ? TemplateNull.NULL : wrapped.getVariable(name); } }
This Environment
returns a NULL
value for variables that were created with the Var- or Set-Directive, so that there are no evaluations that refer to variables. A variable that only refers to expressions with constants or model values can be optimized, all others cannot.
This completes the first implementation of the Partial Template Reduction feature and creates a real USP for FreshMarker. As always, the new version can be found on Maven Central.