“Do you not know that a man is not dead while his name is still spoken?”
― Terry Pratchett, Going Postal
FreshMarker is based on the concepts of another template engine. This is evident from its directives, interpolations, and, of course, its name. Another detail also betrays its origins. The types in the FreshMarker Template Language for collections are sequences and hashes.
Sequences represent lists of values that are either addressed by their position or iterated over, for example, in the list directive. The fact that there is only one type of sequence is not a problem, since all subclasses of java.util.List are mapped to this sequence. Since FreshMarker 2.1.0, subclasses of java.util.Set and java.util.Collection can also be mapped to sequences. In this case, however, there may not be a reliable order of the elements.
Hashes represent mappings from strings to values. The values are thus addressed via a name either with the dot operator hash.name or the hash operator hash['name']. As you can see, the two operators differ only in their syntax and can be used interchangeably. However, the hash operator has a slight advantage over the dot operator because it can be used with a variable: hash[variable]. The variable can now contain the value 'name' and will then return the same result as hash['name']. However, if the variable contains a different string, a different value in the hash is accessed.
All beans and records are mapped to a hash. In this case, the attributes in the beans and records are accessed via their names: beanOrRecord.name or beanOrRecord['name']. Additionally, all subclasses of java.util.Map are mapped to a hash. However, they must have a key of type String. Other Maps are not permitted.
In the next version of FreshMarker, this restriction will be relaxed slightly. One of the reasons for this was the new Template#journal method. This method returns some information about the template as a Map. This Map uses an enum JournalKey for the keys. If you want to know which variables are used in the template, you can access this list using result.get(JournalKey.VARIABLES).
Using an enum as a key has been the method of choice since the introduction of enums, as String values as keys are always prone to errors due to typos.
Unfortunately, you cannot yet use this Map with FreshMarker to generate a nice report from it. To do so, you must first generate a new Map that uses a String instead of the JournalKey enum as the key. Enums are constants with a name, so why not allow such Maps and use the enum’s name for addressing?
After a few attempts, I decided on a solution involving two new subclasses. Previously, the hashes were implemented by the TemplateBean class; this is now split into the TemplateStringBean and TemplateEnumBean classes for the respective key implementations. Normally, I don’t choose this approach because the built-ins are registered per model class. With this solution, the number of registrations for hashes therefore doubles. Fortunately, hashes only have five built-ins, so this issue is negligible here.
The TemplateStringBean class implements the previous hash implementation with String keys.
public class TemplateStringBean extends AbstractTemplateBean<String> {
public TemplateStringBean(Map<String, Object> map, Class<?> type) {
super(map, type);
}
public TemplateObject get(ProcessContext context, String name) {
return getTemplateObject(context, name, name);
}
@Override
public Map<String, Object> map() {
return map;
}
}
Implementation details shared with the TemplateEnumBean class have been moved to the common base class AbstractTemplateBean. The get and map methods remain. The get method returns the value associated with the name and is used by the operators. It delegates to the getTemplateObject method in the base class. The map method is used by the list directive for hashes and simply returns the internal map.
protected TemplateObject getTemplateObject(ProcessContext context, String name, T key) {
TemplateObject templateObject = mapped.get(name);
if (templateObject != null) {
return templateObject;
}
Object object = map.get(key);
if (object == null) {
apped.put(name, TemplateNull.NULL);
return TemplateNull.NULL;
}
if (object instanceof TemplateObject t) {
TemplateObject value = t.evaluateToObject(context);
mapped.put(name, value);
return value;
}
TemplateObject result = context.mapObject(object);
mapped.put(name, result);
return result;
}
The getTemplateObject method in the TemplateBeanMap class is passed the attribute name twice. The first parameter is used to store the extracted value in the cache. This cache is necessary to ensure FreshMarker‘s performance, but it consumes additional memory. The second parameter is used as the key for the internal map. Its generic nature suggests that an enum is likely being used here in the TemplateEnumBean class.
public class TemplateEnumBean extends AbstractTemplateBean<Enum<?>> {
private final Enum<?>[] enumConstants;
public TemplateEnumBean(Map<Enum<?>, Object> map, Class<?> type, Enum<?>[] enumConstants) {
super(map, type);
this.enumConstants = enumConstants;
}
public TemplateObject get(ProcessContext context, String name) {
Enum<?> key = Arrays.stream(enumConstants).filter(e -> e.name().equals(name))
.findFirst().orElseThrow(() -> new ProcessException("enum key not found: " + name));
return getTemplateObject(context, name, key);
}
@Override
public Map<String, Object> map() {
throw new ProcessException("only string key allowed");
}
}
The TemplateEnumBean class has a very similar structure. It differs only slightly due to the use of enums. In the get method, the correct enum constant is determined from the name at the beginning. If it does not exist, an exception is thrown. Otherwise, control is delegated to the getTemplateObject method. To ensure that the possible enum constants are known, they are passed in the constructor. The map method always throws a ProcessException, so there is currently a limitation on the use of enum hashes. They cannot be used in the list directive.
To ensure that the two new classes are used, the CompoundTemplateObjectProvider must be modified. This class maps all collection classes to the appropriate model classes.
@Override
@SuppressWarnings("unchecked")
public TemplateObject provide(TemplateObjectMapper environment, Object o) {
return switch (o) {
case List<?> list -> new TemplateListSequence((List<Object>) list);
case Map<?, ?> map -> getTemplateBean((Map<Object, Object>) map);
case SequencedCollection<?> sequenced -> new TemplateListSequence(new ArrayList<>(sequenced));
case Set<?> set when setAsSequence -> new TemplateListSequence(new ArrayList<>(set));
case Collection<?> collection when collectionAsSequence -> new TemplateListSequence(new ArrayList<>(collection));
default -> o.getClass().isArray() ? provideArray(o) : null;
};
}
@SuppressWarnings("unchecked")
private static TemplateObject getTemplateBean(Map<?, Object> map) {
if (map.isEmpty()) {
return new TemplateStringBean(Map.of(), null);
}
Object first = map.keySet().stream().filter(Objects::nonNull).findFirst().orElse(null);
return switch (first) {
case String stringKey -> new TemplateStringBean((Map<String, Object>) map, null);
case Enum<?> enumKey -> new TemplateEnumBean((Map<Enum<?>, Object>) map, null, enumKey.getDeclaringClass().getEnumConstants());
case null -> throw new ProcessException("unsupported hash key type: null");
default -> throw new ProcessException("unsupported hash key type: " + first.getClass());
};
}
These methods are a good example of why libraries should be implemented using modern Java versions and why compatibility with ancient Java versions—which are no longer maintained by anyone—should be abandoned. Pattern matching for switch statements using guarded patterns allows for compact and readable source code.
The provide method delegates to the getTemplateBean method for maps. This method, in turn, determines which implementation to use based on the first non-null value in the map. This is a trivial but, in most cases, entirely sufficient implementation.
This describes the core of the enum hash implementation, and this feature is available in FreshMarker 2.6.0.