This is an article that reports directly from the bowels of FreshMarker. It is therefore mainly worthwhile for readers who want to know a little more about how FreshMarker works.
The NULL value is not welcome in FreshMarker. An output can only be generated if all interpolations in a template have a non NULL value. A NULL value as the result of an interpolation generates a RuntimeException
and immediately ends the evaluation of the template.
The reason for this rigid mode of operation is obvious. The template engine generates texts that are sent for further processing as e-mails, source code, documentation, etc. A missing variable should not be ignored in the form of an empty or "null"
text, but should be reported as an processing error. For variables that contain optional values, there are the Default and Exist operators that explicitly describe their special behavior in the event of missing values.
When implementing the Partial Template Reduction feature, a weakness in the comparison operator was noticed that needed to be rectified. For a long time, equality within expressions was checked with the TemplateEquality
class
public record TemplateEquality(TemplateObject left, TemplateObject right) implements TemplateBooleanExpression { public TemplateNegative not() { return new TemplateNegative(this); } @Override public TemplateObject evaluateToObject(ProcessContext context) { TemplatePrimitive<?> leftValue = left.evaluate(context, TemplatePrimitive.class); TemplatePrimitive<?> rightValue = right.evaluate(context, TemplatePrimitive.class); return TemplateBoolean.from(leftValue.getValue().equals(rightValue.getValue())); } }
In the evaluateToObject
method, the two operators left
and right
are evaluated to TemplatePrimitive
instances and then compared with each other. This is one of the special features of FreshMarker with regard to comparisons. No comparisons are made between Lists, Ranges, Maps, Beans or Records. There are no operators for this in the template engine data model. So far, there are few use cases for which the addition of FreshMarker would actually be worthwhile.
The evaluateToObject
method has been adapted to support the null
literal, as the TemplateNull
type is not a subclass of TemplatePrimitive
. The null
literal is actually unnecessary in FreshMarker because it is only needed for comparisons and variable values. In both cases, however, you can do without the null
literal.
@Override public TemplateBoolean evaluateToObject(ProcessContext context) { TemplateObject leftObject = left.evaluateToObject(context); TemplateObject rightObject = right.evaluateToObject(context); return TemplateBoolean.from(leftObject.equals(rightObject)); }
Instead of a TemplatePrimitive
, only the TemplateObject
is evaluated. This makes it possible for the leftObject
or rightObject
to also contain the TemplateNull.NULL
instance.
Unfortunately, this solution was too much for the good. In addition to left == null
and null == right
, now also left == right
with missing values for left
, right
or both operators was possible. However, this contradicts the existing NULL
processing in FreshMarker. Only the Default and Exist operators and the comparison with the null
literal may work with NULL
values.
This weakness of the implementation became apparent during the implementation of the Partial Template Reduction feature. If not all variables are set, then all operations that process NULL
values should fail. For the feature, this means that no reduction can be performed at this point. As the comparison operator no longer fails, correct reductions can no longer be distinguished from incorrect ones.
An improved version of the evaluateToObject
method checks the operands for NULL
values. One change is that there are now two NULL
values. null
literals are represented by the value TemplateNull.NULL_LITERAL
and all other NULL
values by TemplateNull.NULL
. A TemplateNull.NULL
value is only permitted if the other value is a TemplateNull.NULL_LITERAL
value.
@Override public TemplateBoolean evaluateToObject(ProcessContext context) { TemplateObject leftObject = evaluate(left, context); TemplateObject rightObject = evaluate(right, context); if (leftObject == TemplateNull.NULL && rightObject != TemplateNull.NULL_LITERAL || leftObject != TemplateNull.NULL_LITERAL && rightObject == TemplateNull.NULL) { throw new ProcessException("null compare only allowed with null literal"); } return TemplateBoolean.from(leftObject.equals(rightObject)); } private TemplateObject evaluate(TemplateObject object, ProcessContext context) { TemplateObject result = object.evaluateToObject(context); if (result.isPrimitive() || result instanceof TemplateNull) { return result; } throw new ProcessException("invalid type " + result.getModelType()); }
To ensure that only TemplatePrimitive
instances are compared alongside the TemplateNull
variants, the result of the evaluation of the operands is checked. At the end of the evaluateToObject
method, the two operands are compared with equals
. The equals
and hashCode
methods have been overwritten so that the two NULL
values return the true
on comparison.
This fixes the weakness of the null equality in FreshMarker and the Partial Template Reduction feature can be implemented further.