Arbeiten mit FreeMarker Templates

„Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.“

Linus Torvalds

Schaut man in irgendeine Code-Basis, dann finden sich immer Fragmente, die zur Generierung von Textdarstellungen dienen. Ob es sich um Inhalt einer Bestätigungs-Email, die Ausgabe von Rechnungsläufen oder eines Statusreports handelt.

Je nach technologischer Reife der Systeme finden sich zur Erzeugung solcher Texte einfache String-Konkatenationen, String.printf Lösungen auf Konstanten oder Dateien, fluent Builder-APIs, eigene Template Engines oder erprobte Lösungen wie etwa FreeMarker.

Prinzipiell arbeiten die String.printf und die Template Engine Lösungen in gleicher Weise. Ein Template mit Platzhaltern wird mit Variableninhalten gefüllt. Die Lösungen unterscheiden sich hauptsächlich in den reichhaltigen Möglichkeiten einer Template Engine gegenüber einem einfachen Formatierungs-Strings.

Der folgende Code-Ausschnitt zeigt einen Formatierungs-String aus einer Properties-Datei.

class.method=\
\u0020\u0020public static Matcher<%1$s> has%2$s(final %3$s %4$s) {%n\
\u0020\u0020\u0020\u0020return has%2$s(equalTo(%4$s));%n\
\u0020\u0020}%n%n

Mit den korrekten vier Parametern befüllt, ergibt sich der Quellcode für eine Methode.

  public static Matcher<Person> hasAge(final Integer age) {
    return hasAge(equalTo(age));
  } 

Obwohl dieser Ansatz funktioniert, hat er doch einige Schwächen. Die Formatierungs-Parameter sind schlecht voneinander zu unterscheiden und führen leicht zu Verwechslungen und die \u0020 und %n\ Zeichen für Einrückungen und Zeilenwechseln stören.

Ein weiterer Nachteil dieser Lösung ist, dass alle Teile der Source-Datei einzeln formatiert werden müssen und in die Datei geschrieben werden müssen.

All diese Probleme lassen sich durch eine Template Engine lösen. Der Hamcrest Matcher Generator nutzt daher auch FreeMarker zur Generierung der Source-Dateien.

Damit die Engine verwendet werden kann, wird eine Konfiguration benötigt.

Configuration cfg = new Configuration(VERSION_2_3_29);
cfg.setClassForTemplateLoading(getClass(), "");
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);
cfg.setWrapUncheckedExceptions(true);
cfg.setFallbackOnNullLoopVariable(false);

cfg.setSharedVariable("author", "Hamcrest Matcher Generator by Jens Kaiser");
cfg.setSharedVariable("version", "1.0.0");

Der obige Code-Ausschnitt erzeugt eine FreeMarker Konfiguration mit diversen Standardeinstellungen. Interessant ist hier die zweite Zeile, in der die Quelle für die Templates definiert wird. In diesem Fall werden die Template-Dateien im selben Package gesucht. Die letzten beiden Zeilen setzen Variablen, die immer in allen Templates zur Verfügung stehen sollen.

Nachdem die Konfiguration erstellt ist, müssen die notwendigen Variablen befüllt werden. FreeMarker erwartet dazu eine Map, deren Schlüssel den Platzhalternamen im Template entsprechen.

String packageName = ...
String sourceType = ...
List<? extends MethodTemplate> methodTemplates = ... 
List<String> imports = ...

Map<String, Object> root = new HashMap<>();
root.put("package", packageName);
root.put("sourceType", sourceTypeName);
root.put("matcherTypeName", sourceTypeName + "Matchers");
root.put("methods", methodTemplates);
root.put("imports", imports);

Die Parameter aus dem zu Anfang dargestellten Formatierungs-String sind hier nicht direkt zu sehen, sie sind in den Instanzen vom Type MethodTemplate verpackt. Hier wird die Möglichkeit der Template Engine genutzt, für eine Liste von Instanzen über Fragmente aus dem Template zu iterieren. Der folgende Ausschnitt aus dem Template zeigt unter #case das Template Fragment das den Formatierungs-String ersetzt.

<#list methods as method>
  <#switch method.template>
...
    <#case "class.method">
  public static Matcher<${sourceType}> has${method.attributeName}(final ${method.returnType} ${method.variableName}) {
    return has${method.attributeName}(equalTo(${method.variableName}));
  }
      <#break>
    <#default>
  </#switch>
</#list>

Mit dem Tags #list kann durch Listen von Variablenwerten iteriert werden und mit #switch/#case/#default verschiedene alternative Template Fragmente genutzt werden. Die Variablenwerte werden über verschiedene Platzhalter eingefügt. Der Platzhalter ${sourceType} fügt den String direkt ein. Platzhalter wie ${method.returnType} greifen auf das Attribute returnType innerhalb der entsprechenden MethodTemplate Instanz zu. FreeMarker bietet hier noch diverse weitere Möglichkeiten die Darstellung der Werte zu manipulieren oder fehlende Werte durch einen Defaultwert zu ersetzen. All dies ist in diesem Szenario aber nicht notwendig.

Durch den Einsatz sprechender Namen für die Platzhalter ist das Fragment auch besser zu lesen und anzupassen.

Wenn Konfiguration und Variablen bereitstehen, muss nur noch das Template geladen und die Ausgabe produziert werden.

PrintWriter out = ...
Map<String, Object> root ...
Template temp = cfg.getTemplate("template.ftl");
temp.process(root, out);

Das Template wird von der Konfiguration von der definierten Quelle mit der Methode getTemplate geladen. Dabei werden geladenen Template Instanzen in einem internen Cache gehalten, um spätere Aufrufe zu optimieren. Zum Ende hin wird, mit der process Methode, die Ausgabe aus Variablen und Template erzeugt und an den angegebenen Writer übergeben.

Der Code zum erstellen der Source-Code Dateien hat sich durch die Verwendung der Template Engine drastisch reduziert und vereinfacht. Außerdem sind die verwendeten Templates übersichtlicher und weniger fehleranfällig.