FreshMarker, eine frische Template Engine für Java

Dieser und weitere Beiträge werden sich mit der Template-Engine FreshMarker beschäftigen. Das Besondere an dieser Template-Engine ist, dass sie noch nicht existiert. Die Beiträge werden, parallel zur Entwicklung der Template-Engine, technische Entscheidungen und verwendete Konzepte beleuchten.

FreshMarker wird keine vollständige Neuentwicklung sein, sondern sich an der Template-Engine FreeMarker orientieren. FreeMarker glänzt mit einer langen Liste von Featuren, hat aber leider einige Schwächen. Aus Sicht des Anwenders fehlt insbesondere die Unterstützung der Java Time API, FreeMarker beherrscht leider nur java.util.Date, java.sql.Date und java.sql.Time. Aus Entwicklersicht ist es unschön anzusehen, dass der Quellcode noch immer auf Java 7/Java 8 basiert.

Damit sind schon einige Anforderungen an die neue Template-Engine formuliert. Unterstützung moderner Java Versionen (Minimum Java 11), Unterstützung der Java Time API und Anlehnung an die FreeMarker Syntax.

Die Entwicklung wird erst schrittweise einige Kernkonzepte bereitstellen und diese dann später vervollständigen. Grundsätzlich soll dabei Wert gelegt werden auf eine einfache, testbare, offene, lose gekoppelte und erweiterbare Architektur.

Ein erster Testfall für die neue Template-Engine ist die Auswertung eines simplen Templates. In diesem Fall enthält es nur einen einfachen Text ohne spezielle Ergänzungen.

@Test
void generateOnlytext() throws IOException, ParseException {
  Configuration configuration = new Configuration()

  TemplateLoader templateLoader = new StringTemplateLoader();
  configuration.registerTemplateLoader(templateLoader);
  templateLoader.putTemplate("test", "the lazy dog jumps over the quick brown fox");

  Template template = configuration.getTemplate("test");
  assertEquals("the lazy dog jumps over the quick brown fox", template.process(Map.of()));
}

Die Anwendung der neuen Template-Engine hält sich eng an ihr Vorbild. Zuerst wird eine Configuration instanziiert und mit dieser ein Template erzeugt. Damit die Configuration ein Template finden kann, wird ein StringTemplateLoader verwendet, der die Sourcen für die Templates in einer Map verwaltet.

public class StringTemplateLoader implements TemplateLoader {
  private final Map<String, TemplateSource> cache = new HashMap<>();

  @Override
  public Optional<TemplateSource> getTemplate(String name) {
    return Optional.ofNullable(cache.get(name));
  }

  public void putTemplate(String name, String content) {
    cache.put(name, new StringTemplateSource(content));
  }

  private static class StringTemplateSource implements TemplateSource {

    final String content;

    public StringTemplateSource(String content) {
      this.content = content;
    }

    @Override
    public Reader getReader(Charset encoding) {
      return new StringReader(content);
    }
  }
}

Das Ergebnis der Template Prozessierung ist der Rückgabewert der Methode process, die als Parameter das Datenmodel als Map übergeben bekommt.

private final BlockFragment rootFragment = new BlockFragment();
private final Configuration configuration;

public void process(Map<String, Object> dataModel, Writer writer) {
  rootFragment.process(configuration.createEnvironment(dataModel), writer);
}

public String process(Map<String, Object> dataModel) {
  StringWriter writer = new StringWriter();
  process(dataModel, writer);
  return writer.toString();
}

Die process Methode existiert in zwei Varianten, die allgemeiner Version besitzt einen Writer als Parameter und schreibt das Ergebnis der Prozessierung in den Writer. Die zweite Variante liefert direkt einen String zurück und nutzt intern die erste Variante mit einem StringWriter.

Wie hier zu erkennen ist, werden intern die Klassen BlockFragment und Configuration genutzt. BlockFragment gehört zu den Klassen die das Interface Fragment implementieren und die statische Struktur des Templates beschreiben. Das Prozessieren eines Templates ist ein Delegieren an das Prozessieren seiner Fragmente. Die Configuration wird genutzt, um für jeden einzelnen Aufruf der process Methode eine unabhängige Verarbeitungsumgebung zu schaffen. Das Erzeugen eines Templates ist aufwendig und sollte daher möglichst nur einmal erfolgen. Durch die unabhängigen Verarbeitungsumgebungen kann eine Template Instanzen einmal erzeugt und beliebig oft verwendet werden.

Noch bleibt die Fragen zu klären, wie ein Template erzeugt wird. Dies wird im nächsten FreshMarker Beitrag erklärt.

Leave a Comment