FreshMarker Random Plug-In

My momma always said, ‘Life was like a box of chocolates. You never know what you’re gonna get.

Forrest Gump

The topic of randomness has visited this blog several times. There was a post about the Voses algorithm and random values in JUnit 5 Test. So why not provide random values in FreshMarker. Since random values are rarely needed in templates, this extension is not realized in FreshMarker itself, but in a separate plugin. In addition, this is a good opportunity to demonstrate the FreshMarker plugin mechanism.

Before we create the plugin for random values, let’s take a closer look at the extension. There are several mechanisms available for embedding random values. The simplest way is to provide template functions that generate random values for a dedicated type.

${randomInteger()}
${10 + randomInteger(10)}
${randomUUID()}

The first two interpolations return Integer values. The first returns a value between Integer.MIN_VALUE and Integer.MAX_VALUE and the second a value between 10 and 20. The third interpolation returns a UUID as a String.

This implementation is functionally complete, easy to create and expandable. Unfortunately, it has one major disadvantage: it is not aesthetically pleasing. Therefore, I have chosen a more attractive approach.

The implementation I prefer provides its own random type, whose values can be converted into the desired types via built-ins. As only one variable of this random type is needed, it is provided as a built-in variable.

${.random?int}
${10 + .random?int(10)}
${.random?uuid}

The second example shows the same functionality as in the first example, but this solution looks more like a FreshMarker solution.

What does the implementation of the plugin look like? First, the implementation of the TemplateRandom model class is required. It must implement the TemplateObject interface and should carry a Random value.

public record TemplateRandom(Random random) implements TemplateObject {
  @Override
  public TemplateObject evaluateToObject(ProcessContext context) {
    return this;
  }
}

With a record implementation, it is only a few lines of code.

Next, we need a PluginProvider implementation that can be loaded by FreshMarker during initialization.

public class RandomPluginProvider implements PluginProvider {
}

The RandomPluginProvider is found by FreshMarker via the service locator mechanism, because the META-INF/services directory contains a file with the name org.freshmarker.core.plugin.PluginProvider, which contains the name of the RandomPluginProvider.

Depending on which PluginProvider methods have been overwritten, the RandomPluginProvider can provide different functionality. The registerBuiltInVariableProviders method must be overwritten so that FreshMarker knows the built-in variable .random.

@Override
public void registerBuiltInVariableProviders(Map<String, Function<ProcessContext, TemplateObject>> providers) {
  providers.put("random", RandomPluginProvider::getRandom);
}

private static TemplateObject getRandom(ProcessContext context) {
  Map<Object, Object> store = context.getStore(TemplateRandom.class);
  return (TemplateObject) store.computeIfAbsent("random", n -> new TemplateRandom(new SecureRandom()));
}

The function that generates the current value of the built-in variable is stored in the providers map. The value of the variable is stored securely in FreshMarker’s context store, away from other variables.

The registerBuildIn method is overwritten so that FreshMarker recognizes built-ins for TemplateRandom values. The respective functions are inserted into the map builtIns. If time permits at some point, the registerBuildIn method is also renamed registerBuiltIns.

@Override
public void registerBuildIn(Map<BuiltInKey, BuiltIn> builtIns) {
  builtIns.put(BUILDER.of("int"), (x, y, e) -> getRandomInteger((TemplateRandom) x, y, e));
  builtIns.put(BUILDER.of("boolean"), (x, y, e) -> getRandomBoolean((TemplateRandom) x));
  builtIns.put(BUILDER.of("item"), (x, y, e) -> getRandomItem((TemplateRandom) x, y, e));
  builtIns.put(BUILDER.of("uuid"), (x, y, e) -> new TemplateString(UUID.randomUUID().toString()));
}

The uuid built-in is a small outlier among the built-ins for random values, because the UUID class provides a random function by default. The other three methods use the Random instance from the variable x and the parameter list y to create TemplateObject instances of the respective type.

At the end of the article, it would of course be nice if you could use your own random variables instead of the default .random variable. This makes it possible to assign a seed to the random variable, which of course does not work with a built-in variable.

Fortunately, FreshMarker has also made provisions for this. With the PluginProvider method registerMapper, mapping functions for custom types can be registered on internal FreshMarker types.

@Override
public void registerMapper(Map<Class<?>, Function<Object, TemplateObject>> mapper) {
  mapper.put(Random.class, x -> new TemplateRandom((Random)x));
  mapper.put(SecureRandom.class, x -> new TemplateRandom((Random)x));
}

For our new plugin, the two types Random and SecureRandom are mapped to TemplateRandom by calling the constructor.

With these additional lines, random values can now also be transferred in the model. The following interpolations are now also possible for a Map.of("abraxas", new Random(23L)) model.

${abraxas?int}
${10 + abraxas?int(10)}
${abraxas?uuid}

If you want to use the new options, you can find the freshmarker-random plugin on Maven Central.

<dependency>
  <groupId>de.schegge</groupId>
  <artifactId>freshmarker-random</artifactId>
 <version>1.1.0</version>
</dependency>

Leave a Comment