“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.