FreshMarker Version Variable Fixes

„Stets findet Überraschung statt, da, wo man’s nicht erwartet hat.“

Wilhelm Busch

FreshMarker is strongly characterised by KISS and YAGNI. This makes the template engine lean and yet powerful. But in some places, the simplicity leads to careless mistakes.

In the first versions, the system variable .version was defined as a constant for the FreshMarker version. Unfortunately, this meant that the version number was regularly forgotten to be updated manually before the release.

To date, the implementation has been as follows.

public record TemplateBuiltInVariable(String name) implements TemplateExpression {

  @Override
  public TemplateObject evaluateToObject(ProcessContext context) {
    return switch (name) {
      case "now" -> new TemplateLocalDateTime(LocalDateTime.now());
      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 TemplateString("1.0.0");
      default -> throw new IllegalStateException("Unexpected value: " + name);
    };
  }
}

When the release version was changed in pom.xml, the string was not always updated. However, as the version has been included in the manifest file for a long time, the implementation can easily be changed.

public record TemplateBuiltInVariable(String name) implements TemplateExpression {

  @Override
  public TemplateObject evaluateToObject(ProcessContext context) {
    return switch (name) {
      case "now" -> new TemplateLocalDateTime(LocalDateTime.now());
      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 TemplateString(getClass().getPackage().getImplementationVersion());
      default -> throw new IllegalStateException("Unexpected value: " + name);
    };
  }
}

With this implementation, the version number comes indirectly from the manifest file. Unfortunately, one of the unit tests fails after this change.

@ParameterizedTest
@CsvSource({
  "test: ${.lang},test: de",
  "test: ${.locale},test: de_DE",
  "test: ${.country},test: DE",
  "test: ${.version},test: 1.0.0",
})
void builtInVariables(String templateSource, String expected) throws ParseException {
  Template template = configuration.getTemplate("test", templateSource);
  assertEquals(expected, template.process(Map.of()));
}

The fourth test no longer works because the automatically generated manifest file does not yet exist. It will be generated in a later step of the build pipeline.

Of course, we can have the affected test executed at a later time. To do this, we need an integration test.

class BuiltInVariableTestIT {

  @Test
  void builtInVariables() throws ParseException {
    Configuration configuration = new Configuration();
    Template template = configuration.getTemplate("test", "test: ${.version}");
    assertEquals("test: 1.0.3-SNAPSHOT", template.process(Map.of()));
  }
}

Unfortunately, the integration test also fails. However, this is due to a peculiarity of the Maven Failsafe Plugin. By default, the integration tests are not executed with the generated JAR archive, but on the unpacked classes like the unit tests. However, the manifest file is located in the JAR archive only.

In order for the JAR archive to be used, the configuration of the plugin must be adjusted.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>3.1.2</version>
  <configuration>
    <classesDirectory>target/${project.artifactId}-${project.version}.jar
    </classesDirectory>
    <systemProperties>
      <property>
        <name>projectArtifactId</name><value>${project.artifactId}</value>
      </property>
    </systemProperties>
  </configuration>
</plugin>

The classesDirectory property no longer points to the classes directory, but directly to the JAR archive.

In addition, a system property is passed to the plugin. The integration test should only run during a Maven build, otherwise no manifest file is available.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIfSystemProperty(named = "projectArtifactId", matches = "freshmarker")
public @interface EnabledIfMavenBuild {
}

The integration test can be switched off with @EnableIfSystemProperty if the projectArtifactId property is missing. We create a more descriptive annotation so that it is still possible to understand what the annotation is used for after a few weeks. The purpose of the @EnableIfMavenBuild should be understandable.

class BuiltInVariableTestIT {

  @Test
  @EnabledIfMavenBuild
  void builtInVariables() throws ParseException {
    Configuration configuration = new Configuration();
    Template template = configuration.getTemplate("test", "test: ${.version}");
    assertEquals("test: 1.0.3-SNAPSHOT", template.process(Map.of()));
  }
}

The new integration test improves the quality of the release because the correct version is now always delivered in the template engine. There is also a small adjustment here so that the README.adoc in the project directory also shows the latest version.

[source,xml]
----
<dependency>
include::pom.xml[lines=7..9]
</dependency>
----

The latest version is always in the pom.xml. So we insert the necessary three lines from the pom.xml into the README.adoc via an include macro.

In the next article, we will rebuild the version information so that it is easier to work with.

1 thought on “FreshMarker Version Variable Fixes”

Leave a Comment