FreshMarker Includes

“By believing passionately in something that still does not exist, we create it. The nonexistent is whatever we have not sufficiently desired.”

Franz Kafka

Some time ago I had the pleasure of giving a talk about FreshMarker at the Java User Group Bielefeld. At this event, the question arose as to why FreshMarker does not have an include directive like FreeMarker. The include directive can be used in FreeMarker to insert additional content into templates.

Like some other directives, this directive was not included in the initial implementation of FreshMarker. FreshMarker was originally intended for small templates where import and include directives were not considered useful. The include directive is discouraged by the fact that it is already not recommended in FreeMarker itself. Instead of using includes to get content into the template, this should be done via macros in imports.

Nevertheless, I was interested in how an include directive can be built into FreshMarker and how much effort is involved in such an implementation. Since I have not yet formed a final opinion about the whereabouts of the include directive in FreshMarker, its implementation should be added as an activatable feature.

However, the corresponding mechanism in the FreshMarker API has so far been missing for an activatable feature. Since I like the Jackson Configuration approach, it should look similar in FreshMarker. The Mimikri Pattern is also a great help in this case.

To avoid making the implementation of features unnecessarily complicated, features should be able to be switched on and off via the TemplateBuilder.

Configuration configuration = new Configuration();
TemplateBuilder builder = configuration.builder().with(IncludeDirectiveFeature.ENABLED)
Template template = configuration.builder().getTemplate("test", "include: <#include 'copyright.fmt');

The example above shows how a FreshMarker feature can be activated using the with method. A corresponding method without deactivates a feature. In order to give plugins the opportunity to provide features, the implementation of the features should also be extendable.

The features are managed in the TemplateFeatures class.

public class TemplateFeatures implements FeatureSet {

    record Entry(int flag, boolean enabled) { }

    private Map<TemplateFeature, Entry> masks = new HashMap<>();

    public void addSwitches(TemplateFeature feature, boolean enabled) {
        if (masks.containsKey(feature)) {
            return;
        }
        masks.put(feature, new Entry(masks.size(), enabled));
    }

    public SimpleFeatureSet create() {
        BitSet bitSet = new BitSet(masks.size());
        masks.values().stream().filter(Entry::enabled).forEach(v -> bitSet.set(v.flag()));
        return new SimpleFeatureSet(bitSet, this);
    }

    int getFlag(TemplateFeature feature) {
        Entry entry = masks.get(feature);
        return entry == null ? -1 : entry.flag();
    }

    @Override
    public boolean isEnabled(TemplateFeature feature) {
        return masks.getOrDefault(feature, new Entry(0, false)).enabled();
    }

    @Override
    public boolean isDisabled(TemplateFeature feature) {
        return !masks.getOrDefault(feature, new Entry(0, false)).enabled();
    }
}

A feature of type TemplateFeature is registered with the method addSwitches. The second parameter of the method specifies whether this feature is switched on or off by default. As each feature is assigned a consecutive number, the current status of the feature can be managed by a bit with that index in a BitSet. A feature can be checked using the isEnabled and isDisabled methods. If a feature does not exist, both methods return false. Otherwise, the BitSet checks whether the respective bit is set or not. Thanks to this implementation, any number of features (i.e. a maximum of 2147483647) can be managed by FreshMarker.

Within the TemplateBuilder and Template instances, however, the features are not queried via TemplateFeature, but via the FeatureSet interface. These are snapshots of the selected features when a TemplateBuilder or Template is created. This ensures that a Template always generates the same output, even if a feature has been switched in the Configuration in the meantime.

Now only the IncludeDirectiveFeature is missing as an implementation of TemplateFeature. We use an enum for this.

public enum IncludeDirectiveFeature implements TemplateFeature {
    ENABLED
}

Initially only with a constant to switch the feature on and off. In order for FreshMarker to recognize the feature, we register it in the configuration.

 templateFeatures.addSwitches(IncludeDirectiveFeature.ENABLED, false);

Everything is now ready to implement the include directive. First, the CongoCC parser must know the necessary construct.

Include #IncludeInstruction :
    (<FTL_DIRECTIVE_OPEN1>|<FTL_DIRECTIVE_OPEN2>)
    <_INCLUDE><BLANK>
    Expression
    LooseTagEnd
;

With this clause in the FreshMarker grammar, the parser recognizes an IncludeInstruction with a path specification in the third node. This is evaluated by the FragmentBuilder.

@Override
public List<Fragment> visit(IncludeInstruction ftl, List<Fragment> input) {
  if (featureSet.isDisabled(IncludeDirectiveFeature.ENABLED)) {
    logger.info("include directive ignored");
    return List.of();
  }

  String path = ftl.get(3).accept(InterpolationBuilder.INSTANCE, null).toString();
  try {
    FreshMarkerParser parser = new FreshMarkerParser(template.getTemplateLoader().getImport(template.getPath(), path));
    parser.setInputSource(path);
    parser.Root();
    Root root = (Root) parser.rootNode();
    new TokenLineNormalizer().normalize(root);
    List<Fragment> fragments = root.accept(this, new ArrayList<>());
    fragments.forEach(template.getRootFragment()::addFragment);
    return input;
  } catch (IOException e) {
    throw new ParsingException("cannot read include: " + path, ftl);
  }
}

This first implementation uses the path specification in the path variable to evaluate the content of the include with a new FreshMarkerParser. This completes the basic implementation for the include directive, but a few details still need to be implemented. More on this in the next article.

Leave a Comment