❝If all you have is a hammer, everything looks like a nail.❞
Abraham Maslow
In FreshMarker, primitive model types can be added to the template engine quite easily. Thanks to the Extension API, only a wrapper class, a mapper and a few built-ins are required to work with the new type. Now that many new types have been added, a small misdevelopment has been eliminated.
Some primitive FreshMarker model types have an internal structure that is passed to the outside via built-ins. An example of such a type is Locale
. Here, country
, country_name
, language
and language_name
can be queried via built-ins.
${.locale?country_name} ${.locale?language_name}
For example, the above template provides the output Germany German
for the current english locale in FreshMarker. Normally, however, we expect a built-in to manipulate a value in some way, such as upper_case
converting a String
to upper case or roman_number
converting a Number
to a String
containing a Roman numeral.
We would expect the simple query of an attribute or information about a data object via the Dot or Hash Operator. The initial example should therefore actually look like this.
${.locale.country_name} ${.locale['language_name']}
Here, the country name is queried using the Dot Operator and the name of the language using the Hash Operator. As you can see, both operators are syntactically different but semantically identical. Both return the attribute with the specified name.
The question that naturally arises now is, why use built-ins and not operators? The answer is simple and shameful, the operators have so far only worked for hash data structures. Hash data structures in FreshMarker are Map
implementations with String
keys, Records and Java Beans. The wrappers for these types implement the TemplateMap
interface.
public interface TemplateMap extends TemplateObject { Map<String, Object> map(); TemplateObject get(ProcessContext context, String name); }
The get
method is used within the classes that are responsible for evaluating the two operators.
public class TemplateDotKey implements TemplateExpression { // ... @Override public TemplateObject evaluateToObject(ProcessContext context) { TemplateObject templateObject = map.evaluateToObject(context); return switch (templateObject) { case TemplateNull templateNull -> templateNull; case TemplateMap templateMap -> templateMap.get(context, dotKey); case null, default -> throw new WrongTypeException("wrong map type: " + dotKey + " " + templateObject); }; } }
The TemplateDotKey
class shown here takes care of processing the Dot Operator. If the object on which the dot operator is to be executed is a TemplateMap
, then the get
method is called.
We now need a solution that allows us to use primitive Freshmarker model types without having to implement TemplateMap
or use heavyweight classes like the Bean or Record wrapper implementations.
If you look too closely at the existing implementations, you sometimes overlook the simple solutions that are offered. In fact, only the get
method from the TemplateMap
interface is required to realize addressing via Dot and Hash Operator. So why not offer a different interface?
public interface DotHashAddressable { TemplateObject get(ProcessContext context, String name); }
The DotHashAddressable
interface only contains the get
method and can replace the TemplateMap
interface in the TemplateDotKey
class. To ensure that TemplateMap
implementations continue to be processed, TemplateMap
simply extends DotHashAddressable
.
public interface TemplateMap extends TemplateObject, DotHashAddressable { Map<String, Object> map(); }
How does this help us with our original problem with the Locale
class? The wrapper class TemplateLocale
must also implement DotHashAddressable
.
public class TemplateLocale extends TemplatePrimitive<Locale> implements DotHashAddressable { // ... public TemplateObject get(ProcessContext context, String name) { return new TemplateString(switch (name) { case "country" -> getValue().getCountry(); case "language" -> getValue().getLanguage(); case "country_name" -> getValue().getDisplayCountry(context.getLocale()); case "language_name" -> getValue().getDisplayLanguage(context.getLocale()); default -> throw new ProcessException("unknown attribute: " + name); }); } }
The get
method returns different values from the Locale
instance depending on the attribute name. An error message is thrown if the name is unknown.
In this way, all primitive FreshMarker model types can now access internal structures without built-ins. This extension is part of FreshMarker 2.0.0 and is initially provided for the Locale
, Enum
and Version
types.