FreshMarker Features

He travels the fastest who travels alone.”

Rudyard Kipling

The follow-up article on FreshMarker Features took a little longer because there was simply no feature that wanted to manifest itself in a plugin. In the FreshMarker Includes article, the features for activating and deactivating functionalities within FreshMarker were presented. The outlook was given that FreshMarker plugins should also be able to contribute their own features. How this can work is explained in this article.

Before we get to the additions to FreshMarker, there is a brief summary of the FreshMarker Feature implementation. The features are managed as instances of implementations of the TemplateFeature interface. In order for FreshMarker to recognize a feature, it must be registered with the TemplateFeatures class. There is a single instance of this class within the Configuration. If a TemplateBuilder is created, it receives a snapshot of the current features as an instance of the FeatureSet class. Features can now be activated and deactivated on this instance. If a Template is created, this in turn receives a snapshot of the FeatureSet from the TemplateBuilder. This ensures that a Template always generates the same output, even if a feature has been changed in the TemplateBuilder or in the Configuration in the meantime.

The features created so far were created directly in FreshMarker.

templateFeatures.addSwitches(IncludeDirectiveFeature.ENABLED, false);
templateFeatures.addSwitches(IncludeDirectiveFeature.LIMIT_INCLUDE_LEVEL, true);

This interface has been modified slightly in FreshMarker 1.7.1. A TemplateFeature uses its isEnabledByDefault method to signal whether it is initially enabled or disabled. For this reason, the second parameter is no longer necessary for addSwitches. The addFeatures method can be used instead.

templateFeatures.addFeatures(IncludeDirectiveFeature.class);

However, in order for plugins to contribute features, we have to lead the registration of new features to the plugins. The easiest way to do this is to extend the PluginProvider interface. Each plugin implements this interface and different things are configured depending on the overridden methods.

The following method can be overridden by plugins to register your own built-ins in the configuration.

default void registerBuildIn(Map<BuiltInKey, BuiltIn> builtIns) {

}

An additional method is required to register your own features.

default void registerFeatures(TemplateFeatures features) {

}

A plugin can register its own features using the registerFeatures method. The following DemoPlugin with a DemoFeature shows the code required to register the feature.

public DempPlugin implements PluginProvider {
  public enum DemoFeature implements TemplateFeature {
    DEMO
  }

  public void registerFeatures(TemplateFeatures features) {
    features.addFeatures(DemoFeature.class);
  }
}

The DEMO feature is added to the TemplateFeatures instance via addFeatures. As the isEnabledByDefault method has not been overwritten, the DEMO feature is activated at the start and can be deactivated in the TemplateBuilder via without(DemoFeature.DEMO).

A plugin that only registers one feature is a little useless, so we still need PluginProvider methods that allow further configurations with the help of all registered TemplateFeature.

default void registerBuildIn(Map<BuiltInKey, BuiltIn> builtIns, FeatureSet features) {
  registerBuiltIn(builtIns);
}

If a plugin overwrites this registerBuildIn method, then a FeatureSet is available via which all previously registered TemplateFeatures can be queried. As the registerFeature method is called beforehand, the TemplateFeatures are contained by the plugin.

public void registerPlugin(PluginProvider provider) {
  provider.registerFeatures(templateFeature)
  Map<BuiltInKey, BuiltIn> registerBuiltIns = new HashMap<>(this.builtIns);
  provider.registerBuildIn(registerBuiltIns, templateFeature);
  this.builtIns = registerBuiltIns;
  // ...
}

Only the new registerBuildIn method is called by the Configuration during registration. Existing plugins that implement the old registerBuildIn method can still be used because the default implementation of the new method simply calls the old one.

This means that support for features in plugins has already been implemented and only the plugins with features are still missing.

The features for plugins and more can be tried out in the new version 1.7.1 of FreshMarker.

Leave a Comment