“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.