Null Equality in Templates

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.

Leave a Comment