FreshMarker Extension API

„No one in the brief history of computing has ever written a piece of perfect software. It’s unlikely that you’ll be the first.“

Andy Hunt

The FreshMarker Plug-In Mechanism was one of the first features to be realized for the template engine. The idea behind it was simple. With a proper plug-in mechanism, a new feature can be easily realized and integrated. If it fits the core functionality, it stays in FreshMarker otherwise it becomes an independent plug-in. This is how FreshMarker File, FreshMarker Money and FreshMarker Random were created. It also created the internal FreshMarker support for Version, Locale, Year, MonthYear, MonthDay, Duration, Period and some other classes.

However, the use of the Plug-in Mechanism repeatedly shows that this mechanism is not yet fully developed. On the one hand, the implementation of a single interface is quite simple, but many dependencies accumulate at one point. Although a developer has the option of using multiple implementations, the temptation is great to leave it at one. Within FreshMarker, the mechanism also resulted in a high degree of consistency in the code.

Another weakness is the poor differentiation between the Plug-in Mechanism and the internal classes of the template engine. The unsuccessful attempt to introduce the Java Module System in FreshMarker shows that some changes are still necessary here.

The idea that a clean Extension API, such as the one provided by JUnit 5, could replace the Plug-in Mechanism has been around for a long time. The tasks of an extension in a template engine are quite different from those in a test framework, which is why the FreshMarker Extension API has some similar-sounding identifiers, but the functionality and provision are very different.

Two major hurdles in the development of the Extension API are the compatibility of the next version with the current version and some gaps in the Extension API that have not yet been closed.

Anyone using a third-party library does not want to be repeatedly disrupted in their actual work by breaking changes in their API. An API should therefore offer the change as smoothly as possible. A new API should be offered in parallel to the existing API so that developers can try out the new API and switch if they are interested. This also gives the developer time to plan the necessary adjustments. Nothing is more annoying than feeling time pressure because the necessary constellation of libraries no longer works together in your own project.

At the moment, the Extension API is not yet final in every detail. The right concepts for how to handle the internal FreshMarker classes within the extensions are still missing. Should new concepts be introduced here, or should only the implementations be hidden behind interfaces? The decision will probably only be made in a later version of Freshmarker.

The currently planned path for the Extension API is the change from the current version 1.7.5 to version 1.8.0 with the introduction of the Extension API. Parallel operation of the Extension API with the Plugin Mechanism will take place up to version 2.0.0. Starting with version 2.0.0, the old Plug-in Mechanism will no longer be available.

What can we currently report about the new Extension API? The Java Service Loader remains the central mechanism of the FreshMarker configuration loading. However, no PluginProvider instances are loaded, but Extension instances.

Extension is an interface with a single method.

package org.freshmarker.api.extension;

import org.freshmarker.api.FeatureSet;

public interface Extension {
    /**
     * This method can be used, to modify this {@link Extension} by a feature from the current feature set.
     *
     * @param featureSet the current feature set
     */
    default void init(FeatureSet featureSet) {

    }
}

As you can see, this interface is in its own package org.freshmarker.api.extension, so hopefully nothing stands in the way of a modular future. In addition, the init method is a default method so that an extension does not have to implement this method. A FeatureSet is provided in the init method, which comes from the org.freshmarker.api package. Previously, the FeatureSet resided in an internal package so that there would be no breaking change here, the previous FeatureSet becomes a subinterface of the new one.

@Deprecated(since = "1.8.0", forRemoval = true)
public interface FeatureSet extends org.freshmarker.api.FeatureSet {

}

All possible extensions inherit from the central interface Extension. The BuiltInProvider interface can now be implemented to add your own BuiltIns to the template engine.

/**
 * An {@link Extension} to add new built-ins.
 */
public interface BuiltInProvider extends Extension {
    /**
     * Returns a register of built-ins
     * @return a register of built-ins
     */
   Register<Class<? extends TemplateObject>, String, BuiltIn> provideBuiltInRegister();
}

A BuiltInProvider implements the provideBuiltInRegister method and can therefore register any number of BuiltIns in the template engine. The Register type is a simplification compared to a double-nested Map. The double-nested Map would be necessary because a BuiltInProvider can provide BuiltIns for several model types.

Two classes help with the implementation of the provideBuilInRegister method. The BuiltInRegister class is an implementation of the Register interface and can be used for registers with multiple model types.

BuiltInRegister register = new BuiltInRegister();
register.add(TemplateVersion.class, "is_before", (x, y, e) -> version(x, e).isBefore(parameter(y, e)));
register.add(TemplateString.class, "version", (x, y, e) -> new TemplateVersion(x.evaluate(e, TemplateString.class).toString()));
register.add(TemplateLocale.class, "language", (x, y, e) -> locale(x, e).getLanguage());
register.add(TemplateNull.class, "empty_to_null", BuiltIn.identity());

In this example, four BuiltIns for four different model types are added to the register variable.

The other Register implementation SingleTypeBuiltInRegister can be used if only a single model type is used.

SingleTypeBuiltInRegister register = new SingleTypeBuiltInRegister(TemplateString.class);
register.add("upper_case", (x, y, e) -> upperCase(x, e));
register.add("lower_case", (x, y, e) -> lowerCase(x, e));
register.add("capitalize", (x, y, e) -> capitalize(x, e));       

The model type is injected to the instance in the constructor. An extra add method without the type parameter can then be used.

In addition to the BuitInProvider, there is the BuiltInVariableProvider for own built-In variables, the FormatterProvider for custom Formatter, the FunctionProvider for custom TemplateFunctions, the OutputFormatProvider for custom OutputFormat implementation, the TemplateFeatureProvider for custom TemplateFeatures, the TemplateObjectProviders for custom TemplateObjectProvider implementation, the TypeMapperProvider for custom TypeMapper implementations and the UserDirectiveProvider for custom UserDirective implementations.

In addition to providing the new Extension interfaces, the internal configuration has also been rebuilt. The Configuration class remains the central point of FreshMarker configuration but lot of code is moved to other classes. The ExtensionRegistry and the PluginProviderRegistry have been introduced. The first manages the new extensions, while the second is responsible for the so far existing Plugin Providers. This cleans up the code base and makes it easier to remove the plugin mechanism at a later date.

Leave a Comment