FreshMarker with Optionals

“Pain is inevitable. Suffering is optional.”

Haruki Murakami

Agile software development is characterized, among other things, by the fact that features are developed on demand. This is one of the reasons why some features are not available in FreshMarker or are only added very late.

Examples of late additions are the Include- and Import-Directives and the support of the StringBuilder and StringBuffer data types. For the former, the use case was not planned at the start of development and for the latter, support was simply forgotten. Another forgotten data type is the Optional class, which is now finally to be supported.

The Optional class is a special data type. It makes dealing with the presence or absence of a value safer and more explicit without directly representing a concrete value itself. The Optional data type as a monad makes it possible to work safely and elegantly with optional values by providing an expressive and comprehensible way of handling errors and processing possibly absent values without explicit null checks.

Optional values are not supported in the current FreshMarker version. If the template engine detects an optional value in the data model, processing ends with the corresponding exception.

The implementation of the Optional class should not bring any unnecessary ballast with it. The Optional is only a construct to represent the presence or absence of a value. In FreshMarker, the presence or absence is realized via the Existence and the Default Operator. It is therefore not necessary to define optionals as a separate model type; instead, the values they contain are transferred to the model.

The following example shows the template for a model variable optional on the left, which contains the value Optional.of(42).

${optional}
${optional??}
${optional!23}
42
yes
42

The first interpolation returns the value 42, because FreshMarker does not work with the Optional instance, but with the contained Integer instance. The Existence Operator returns true, which is converted to yes in the output. In this case, the Default Operator also returns 42 because the value exists.

${optional??}
${optional!23}
no
23

If the model variable optional contains the value Optional.empty(), the output changes. The first interpolation was omitted because it throws an exception for a missing value. The Existence Operator returns false, which is converted to the output no. As the value is not set, the Default Operator returns 23 in this case.

To work within the template engine with a value that does not exist in the final version in the model, there is a suitable place. FreshMarker supports lazy values, i.e. the actual value is only determined and used when the template engine accesses such a value. To do this, the model variable is checked to see if it is of the TemplateObjectSupplier type before it is wrapped..

Object current = o instanceof TemplateObjectSupplier<?> templateObject ? templateObject.get() : o;

Here the get method is called on the Supplier and the actual value is determined. What could be more obvious than to check the Optional type at this point.

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

The implementation has become a little longer, the evaluation of the Optional has been added. If the Optional is empty, the value TemplateNull.NULL is used, otherwise the value from the Optional.

The implementation of the feature is now complete and the optional feature will be supported in the next version of the template engine.

Leave a Comment