Structured Primitives

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

Leave a Comment