FreshMarker Version Variable Rebuild

„Glücklich – wer darauf bedacht, wie man Ander’n Freude macht.“

Wilhelm Busch

The last article described how the .version variable in FreshMarker can always be kept up to date. In this article, we want to simplify the use of the variable.

As the .version variable was previously of the string type, it was only possible to work with the string comparison operators and built-ins.

<#if .version == '1.0.2'>
current
<#elseif .version?startsWith('1.0.')>
1.0
<#else>
other
</#if>

In this example, the major and minor versions are first compared with a constant and then with the build-ins startsWith. To use complex comparisons of the major, minor and patch versions, the string representation is unfavourable.

In the future the .version variable is given its own type which has some helpful built-ins. The built-ins is_after and patch are shown in the following example.

<#if .version?is_after('1.1.0')>
1.1
<#elseif .version?is_after('1.0.1')>
1.0
</#if>
patch: ${.version?patch}

The built-in is_after is used to check whether the current version is after the version in the built-in parameter. The parameter can be specified as a string or version. In addition to is_after, there is also is_equal and is_before. The built-in patch returns the patch part of the current version as an integer. In addition, the major and minor part of the version can be returned as integers via the built-ins major and minor.

In order to be able to use a separate type for the versions, we first need a TemplatePrimitive implementation.

public class TemplateVersion extends TemplatePrimitive<Version> {
  public TemplateVersion(String value) {
    super(Version.byString(value));
  }

  public TemplateNumber major() {
    return new TemplateNumber(getValue().major());
  }

  public TemplateBoolean isBefore(TemplateVersion value) {
    return TemplateBoolean.from(getValue().isBefore(value.getValue()));
  }

  // ...
}

The TemplateVersion class uses a record with the attributes major, minor and patch. There are corresponding methods in the class for all built-ins.

The built-in methods still need to be registered. The PluginProvider implementations exist in FreshMarker for this purpose. The following SystemPluginProvider registers all version build-ins.

public class SystemPluginProvider implements PluginProvider {
  private static final BuiltInKeyBuilder<TemplateVersion> VERSION = new BuiltInKeyBuilder<>(TemplateVersion.class);
  private static final BuiltInKeyBuilder<TemplateString> STRING = new BuiltInKeyBuilder<>(TemplateString.class);

  @Override
  public void registerBuildIn(Map<BuiltInKey, BuiltIn> builtIns) {
    register(builtIns, VERSION.of("is_before"), (x, y, e) -> version(x, e).isBefore(parameter(y, e)));
    register(builtIns, VERSION.of("is_after"), (x, y, e) -> version(x, e).isAfter(parameter(y, e)));
    register(builtIns, VERSION.of("is_equal"), (x, y, e) -> version(x, e).isEqual(parameter(y, e)));
    register(builtIns, VERSION.of("major"), (x, y, e) -> version(x, e).major());
    register(builtIns, VERSION.of("minor"), (x, y, e) -> version(x, e).minor());
    register(builtIns, VERSION.of("patch"), (x, y, e) -> version(x, e).patch());
    register(builtIns, STRING.of("version"), (x, y, e) -> x.evaluateToObject(e).asString().map(TemplatePrimitive::toString)
                .map(TemplateVersion::new).orElseThrow());
  }

  private static TemplateVersion version(TemplateObject x, ProcessContext e) {
    return x.evaluate(e, TemplateVersion.class);
  }

  private static TemplateVersion parameter(List<TemplateObject> objects, ProcessContext context) {
    TemplateObject value = objects.getFirst().evaluateToObject(context);
    return switch (value) {
      case TemplateVersion version -> version;
      case TemplateString string -> new TemplateVersion(string.toString());
      default -> throw new IllegalStateException("invalid type: " + value.getModelType());
    };
  }

  private void register(Map<BuiltInKey, BuiltIn> buildIns, BuiltInKey builtInKey, BuiltInFunction function) {
    buildIns.put(builtInKey, new FunctionalBuiltIn(function));
  }
}

In addition to the built-ins presented above, a further built-in is added for the string type. With version, a version is created from a string. Finally, the .version variable only needs to be created as a TemplateVersion variable instead of a TemplateString variable.

public record TemplateBuiltInVariable(String name) implements TemplateExpression {

  @Override
  public TemplateObject evaluateToObject(ProcessContext context) {
    return switch (name) {
      case "now" -> new TemplateLocalDateTime(LocalDateTime.now()).at(context);
      case "locale" -> new TemplateString(context.getEnvironment().getLocale().toString());
      case "country" -> new TemplateString(context.getEnvironment().getLocale().getCountry());
      case "lang" -> new TemplateString(context.getEnvironment().getLocale().getLanguage());
      case "version" -> new TemplateVersion(getClass().getPackage().getImplementationVersion());
      default -> throw new IllegalStateException("Unexpected value: " + name);
    };
  }
}

This is all that is required to implement the new features for the .version variable. They can then be used from version 1.1.0.

Leave a Comment