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.util.Date,
java.sql.Date
java.sql.Date und
java.sql.Time
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@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()));
}
@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())); }
@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
Configuration instanziiert und mit dieser ein
Template
Template erzeugt. Damit die
Configuration
Configuration ein
Template
Template finden kann, wird ein
StringTemplateLoader
StringTemplateLoader verwendet, der die Sourcen für die Templates in einer
Map
Map verwaltet.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
}
}
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); } } }
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
process, die als Parameter das Datenmodel als
Map
Map übergeben bekommt.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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();
}
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(); }
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
process Methode existiert in zwei Varianten, die allgemeiner Version besitzt einen
Writer
Writer als Parameter und schreibt das Ergebnis der Prozessierung in den
Writer
Writer. Die zweite Variante liefert direkt einen
String
String zurück und nutzt intern die erste Variante mit einem
StringWriter
StringWriter.

Wie hier zu erkennen ist, werden intern die Klassen

BlockFragment
BlockFragment und
Configuration
Configuration genutzt.
BlockFragment
BlockFragment gehört zu den Klassen die das Interface
Fragment
Fragment implementieren und die statische Struktur des Templates beschreiben. Das Prozessieren eines Templates ist ein Delegieren an das Prozessieren seiner Fragmente. Die
Configuration
Configuration wird genutzt, um für jeden einzelnen Aufruf der
process
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
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.

1 thought on “FreshMarker, eine frische Template Engine für Java”

Leave a Comment