Null Aware Built-In Handling 

“Step by step and the thing is done!

Charles Atlas

As soon as the handling of optional values is built into FreshMarker, another interesting feature appears, which could be added with tiny changes. It was implemented in three steps, with the next step only becoming apparent once the previous step had been implemented. The whole thing started with the fact that empty optionals should be processed slightly differently than null values.

The approach for optional values in the last article was quickly implemented, but left the downer that empty optional values are interpreted as errors due to absent values. This gave rise to the idea of changing the behavior by adding another null value. This procedure has already been used another time to realize null literals in FreshMarker.

When integrating null literals, it became necessary to distinguish whether an instance of type TemplateNull is the result of an expression or the realization of a null literal. A comparison with null values should only be permitted if at least one of them is a null literal. As all TemplateNull instances were previously actually the instance TemplateNull.NULL, a further instance TemplateNull.NULL_LITERAL was introduced for this purpose.

For empty optional instances, there is now also TemplateNull.NULL_OPTIONAL. This slightly changes the evaluation of the optional from the previous article.

Object current;
if (o instanceof Optional<?> optional) {
  if (optional.isEmpty()) {
    return TemplateNull.NULL_OPTIONAL;
  }
  current = optional.get();
} else if (o instanceof TemplateObjectSupplier<?> templateObjectSupplier) {
  current = templateObjectSupplier.get();
} else {
  current = o;
}

Instead of TemplateNull.NULL, the evaluation now returns TemplateNull.NULL_OPTIONAL. This new value must also be used so that processing in the template engine changes.

One behavior in the template engine that does not match the nature of Optional is the processing in the built-ins. The built-ins operate on the variable value and can also process the values further via a chain of built-ins. This chained processing stops as soon as one of the built-ins encounters a null value. This is not the case with Optional. With their map and filter methods, care is taken to ensure that processing is not aborted when null values are encountered. This should also be possible with optional values in the built-ins. The evaluateToObject method in the TemplateBuildIn class is responsible for evaluating a built-in.

The input value of the built-in is calculated in the first line. Then it is checked whether there is a built-in implementation for this value and then this implementation is used.

@Override
public TemplateObject evaluateToObject(ProcessContext context) {
  TemplateObject result = expression.evaluateToObject(context);
  if (result == TemplateNull.NULL_OPTIONAL) {
    return result;
  }
  try {
    return context.getBuiltIn(result.getClass(), name).apply(result, parameter, context);
  } catch (UnsupportedBuiltInException e) {
    if (!(result instanceof TemplateLooper)) {
      throw e;
    }
    result = result.evaluateToObject(context);
    return context.getBuiltIn(result.getClass(), name).apply(result, parameter, context);
  }
}

Lines two to four are new additions; if the input value is TemplateNull.NULL_OPTIONAL, then this value is returned. This is actually the end of the handling of optional values in the build-ins, but thanks to the possibilities of the FreshMarker feature, we can still modify the behavior a little. In the second step, we now add the configuration.

public enum BuiltinHandlingFeature implements TemplateFeature {
    IGNORE_OPTIONAL_EMPTY, IGNORE_NULL;

    @Override
    public boolean isEnabledByDefault() {
        return false;
    }
}

A new TemplateFeature implementation BuiltinHandlingFeature provides two new features. The new implementation can be switched on and off with the IGNORE_OPTIONAL_EMPTY feature. The IGNORE_NULL feature is intended to provide similar behavior for TemplateNull.NULL values.

The implementation changes slightly as a result of the feature. The handling of null values is additionally switched via a boolean attribute. These attributes are activated or deactivated via the corresponding feature.

@Override
public TemplateObject evaluateToObject(ProcessContext context) {
  TemplateObject result = expression.evaluateToObject(context);
  if (result == TemplateNull.NULL_OPTIONAL && ignoreOptionalNull) {
    return result;
  }
  if (result == TemplateNull.NULL && ignoreNull) {
    return result;
  }
  try {
    return context.getBuiltIn(result.getClass(), name).apply(result, parameter, context);
  } catch (UnsupportedBuiltInException e) {
    if (!(result instanceof TemplateLooper)) {
      throw e;
    }
    result = result.evaluateToObject(context);
    return context.getBuiltIn(result.getClass(), name).apply(result, parameter, context);
  }
}

The attributes are transferred to the TemplateBuiltIn instances when the template is created in the InterpolationBuilder.

@Override
public TemplateBuiltIn visit(BuiltIn expression, Object input) {
  boolean ignoreOptionalEmpty = featureSet.isEnabled(BuiltinHandlingFeature.IGNORE_OPTIONAL_EMPTY);
  boolean ignoreNull = featureSet.isEnabled(BuiltinHandlingFeature.IGNORE_NULL);
  Token buildInName = (Token) expression.get(1);
  if (expression.size() < 3) {
    return new TemplateBuiltIn(buildInName.toString(), (TemplateObject) input, List.of(), ignoreOptionalEmpty, ignoreNull);
  }
  List<TemplateObject> parameter = new ArrayList<>();
  Node child = expression.get(3);
  if (child instanceof PositionalArgsList) {
    child.accept(argsListBuilder, parameter);
  } else {
    parameter.add(child.accept(this, null));
  }
  return new TemplateBuiltIn(buildInName.toString(), (TemplateObject) input, parameter, ignoreOptionalEmpty, ignoreNull);
}

Now we come to the creative third step in the implementation. How would it be if the configuration via feature were not necessary to use the new null handling in the template? So we are still looking for a way to configure it within the template.

The template "${value?trim?length!0}" does not work with a null value or an empty optional. In both cases, an exception is thrown without the corresponding features. An additional built-in operator can be used to set the attributes in the built-in instance.

@Override
public TemplateBuiltIn visit(BuiltIn expression, Object input) {
  boolean pipe = expression.getFirst().getType() == TokenType.BUILT_IN2;
  boolean ignoreOptionalEmpty = pipe || featureSet.isEnabled(BuiltinHandlingFeature.IGNORE_OPTIONAL_EMPTY);
  boolean ignoreNull = pipe || featureSet.isEnabled(BuiltinHandlingFeature.IGNORE_NULL);
  Token buildInName = (Token) expression.get(1);
  if (expression.size() < 3) {
    return new TemplateBuiltIn(buildInName.toString(), (TemplateObject) input, List.of(), ignoreOptionalEmpty, ignoreNull);
  }
  List<TemplateObject> parameter = new ArrayList<>();
  Node child = expression.get(3);
  if (child instanceof PositionalArgsList) {
    child.accept(argsListBuilder, parameter);
  } else {
    parameter.add(child.accept(this, null));
  }
  return new TemplateBuiltIn(buildInName.toString(), (TemplateObject) input, parameter, ignoreOptionalEmpty, ignoreNull);
}

In this implementation, the built-in operator is also checked. With the TokenType.BUILT_IN the previous processing takes place, with the TokenType.BUILT_IN2 both attributes are set to true. Now all that is missing is a suitable operator. Unfortunately, ?? is already assigned as existence operator, the pipe symbol | is already assigned as or operator and || is already assigned as conditional or operator.

For the first experiments we use !! and ->. The templates "${value!!trim!!length!0}" and "${value->trim->length!0}" allow us to configure the new possibilities of Null Aware Built-In Handling separately for each individual built-in.

Leave a Comment