“Any code of your own that you haven’t looked at for six or more months might as well have been written by someone else.”
Eagleson’s law
Fasst drei Jahre nach dem ersten Beitrag zum eigenen API MarktDown ergibt sich ein zweiter Beitrag zum Thema. Bei der Durchsicht der eigenen Projekte fiel auf, dass in dem Projekt rest-markdown-plugin noch immer FreeMarker als Template-Engine verwendet wurde. Die erste Ad-Hoc Umstellung der Template-Engine auf FreshMarker scheiterte jedoch kläglich.
Die Gründe für das Scheitern waren schnell ausgemacht. Die Template-Engine FreshMarker unterstützte nicht alle in der Bibliothek verwendeten Features von FreeMarker. Das ist aber auch nicht besonders erschreckend oder unerwartet, da FreshMarker nie als vollständiger Ersatz für FreeMarker angedacht war.
Bei den fehlenden Features handelte es sich um das String Built-In first_cap
, Support für den Datentyp java.net.URI
und die Möglichkeit mit der List-Direktive über Hashes zu iterieren.
Im Sinne der FreshMarker Philosophie sollte der Großteil der Vorbereitungen im Java Code erfolgen. Das Built-In first_cap
erzeugt aus einem kleingeschriebenen Wort ein Wort mit großen Anfangsbuchstaben. Ist das Feature nicht zwingend notwendig, wie in diesem Fall, dann kann einfach darauf verzichtet werden. Im anderen Fall gibt es noch immer die Möglichkeit, den Text im Java Code anzupassen.
Diese Möglichkeit wird auch bei der fehlenden Unterstützung des Datentyps java.net.URI
genutzt. Normalerweise werden nicht explizit unterstützte Datentypen mit Hilfe der TemplateBean
Klasse verarbeitet. Ein kleiner Sicherheitsmechanismus in FreshMarker verbietet dies jedoch für Klassen unterhalb des Package java
. In einer späteren Version von FreshMarker wird dieser Mechanismus sicherlich noch modifiziert.
Bei der Aufbereitung der Daten können Instanzen vom Typ java.net.URI
mit der Methode toString
in den unterstützten Typ String
umgewandelt werden.
Problematischer ist das Fehlen von List-Direktiven über Hashes. Da der API Generator über diverse Maps und Beans iteriert wäre eine Umstellung auf Listen nicht nur zeitaufwendig sondern auch überaus hässlich.
Die erste Idee für eine Implementierung ist schnell gefunden. Da sowohl Maps als auch Beans durch den Typ TemplateMap
dargestellt werden, kann eine Implementierung für Hashes auf dem EntrySet
der hinterlegten Map
arbeiten. Das FreshMarker auf einem statisches Datenmodel arbeitet, kann sich das EntrySet
, nachdem es als Ergebnis einer Auswertung vorliegt, nicht mehr ändern.
Bevor es an die Implementierung geht, gilt es noch ein Problem bei der Syntax im Template zu beheben. Listen werden in FreshMarker über die List-Direktive mit oder ohne Loop Variable angegeben. Die ersten beiden Zeilen im folgenden Beispiel zeigen die bisherigen implementierungen in FreshMarker und die dritte Zeile die FreeMarker List-Direktive über Hashes.
<#list sequence as item>${item}</#list> <#list sequence as item, looper>${looper?counter} ${item}</#list> <#list sequence as key, value>${key} ${value}</#list>
Leider stimmen die letzten beiden Zeilen syntaktisch überein und die Template Engine kann nicht einfach entscheiden, welche Form gewünscht ist. Um die Implementierung einfach zu halten wird die Syntax für die Loop Variable angepasst. Mit der veränderten Syntax ist es einfach, die insgesamt vier Varianten der List-Direktive zu unterscheiden.
<#list sequence as item>${item}</#list> <#list sequence as item with looper>${looper?counter} ${item}</#list> <#list sequence as key, value>${key} ${value}</#list> <#list sequence as key, value with looper>${looper?counter} ${key} ${value}</#list>
Die Angabe einer Loop Variable wird immer mit dem reserviert Bezeichner with
eingeleitet. Die dazugehörige Ergänzung der JavaCC 21 Grammatik ist im folgenden dargestellt.
List #ListInstruction : (<FTL_DIRECTIVE_OPEN1>|<FTL_DIRECTIVE_OPEN2>) <LIST><BLANK> Expression <AS> <IDENTIFIER> [ <COMMA> <IDENTIFIER> ] [ <WITH> <IDENTIFIER> ] <CLOSE_TAG> Block DirectiveEnd("list") ;
Ein optionaler Block in der fünften Zeile, definiert einen IDENTIFIER
Token, dem ein WITH
Token vorangestellt ist.
Seit geraumer Zeit existiert der JavaCC 21 Nachfolger CongoCC. Beizeiten wird auch FreshMarker diesen Parser Generator nutzen.
Da sich die Grammatik leicht geändert hat, muss die entsprechende Methode im FragmentBuilder
Visitor auch angepasst werden.
@Override public BlockFragment visit(ListInstruction ftl, BlockFragment input) { TemplateObject list = ftl.getChild(3).accept(interpolationBuilder, null); int looperIndex = ftl.getChild(6).getTokenType() == TokenType.COMMA ? 9 : 7; int blockIndex = looperIndex; String looperIdentifier = null; if (ftl.getChild(looperIndex - 1).getTokenType() == TokenType.WITH) { looperIdentifier = ((IDENTIFIER) ftl.getChild(looperIndex)).getImage(); blockIndex += 2; } BlockFragment block = ftl.getChild(blockIndex).accept(this, new BlockFragment()); if (ftl.getChild(6).getTokenType() == TokenType.COMMA) { String keyIdentifier = ((IDENTIFIER) ftl.getChild(5)).getImage(); String valueIdentifier = ((IDENTIFIER) ftl.getChild(7)).getImage(); input.addFragment(new HashListFragment(list, keyIdentifier, valueIdentifier, looperIdentifier, block, ftl)); } else { String identifier = ((IDENTIFIER) ftl.getChild(5)).getImage(); input.addFragment(new SequenceListFragment(list, identifier, looperIdentifier, block, ftl)); } return input; }
Die Auswertung für die ListInstruction
unterscheidet sich, wenn sich an Position 6 ein COMMA
Token befindet. Genau dann handelt es sich um eine List Direktive für Hashes. Basierend auf dieser Information findet sich dann die Loop Variable und der auszuführende Block.
Da sich die neue Implementierung leicht vom bisherigen ListFragment
unterscheidet, wandert der gemeinsame Code in die AbstractListFragment
Klasse und davon erben die neuen SequenceListfragment
und HashListFragment
.
public abstract class AbstractListFragment<T> implements Fragment { protected final TemplateObject list; protected final String looperIdentifier; protected final BlockFragment block; protected final ListInstruction ftl; public AbstractListFragment(TemplateObject list, String looperIdentifier, BlockFragment block, ListInstruction ftl) { this.list = list; this.looperIdentifier = looperIdentifier; this.block = block; this.ftl = ftl; } protected void processLoop(ProcessContext context, AbstractTemplateLooper<T> looper, Environment hashEnvironment) { Environment environment = context.getEnvironment(); try { context.setEnvironment(new VariableEnvironment(hashEnvironment)); for (int i = 0; i < looper.size(); i++) { block.process(context); looper.increment(); } } finally { context.setEnvironment(environment); } } }
Die Subklassen unterscheiden sich geringfügig. Für Hashes werden zwei Identifier benötigt. Einer für den Key und einer für den Value. Bei Sequences gibt es nur einen Identifier für das Item. Der Rückgabewert der Auswertung bei Hashes ist immer eine Instanz von TemplateHash
, die das Key/Value Paar zusammenhält. Bei Sequences ist es immer die Subklasse von TemplateObject
, die den Datentyp des Item repräsentiert.
Mit diesen Änderungen können nun die API Template mit ihren List-Direktiven über Hashes auch von FreeMarker bearbeitet werden.
==== Responses [%header,cols="1l,2l,2l,3"] |=== | Code | Message | Content-Type | Schema <#list endpoint.responses as code, response> <#if (response.content)??> <#list response.content as type, content> | ${code} | ${response.description} | ${type} | <#if (content.schema.schema.name)??><<${content.schema.schema.name}>></#if> </#list> <#else> | ${code} | ${response.description} | | </#if> </#list> |===
Wie immer ist auf Maven Central eine neue Version der Bibliotheken zu finden.
<dependency> <groupId>de.schegge</groupId> <artifactId>freshmarker</artifactId> <version>0.5.0</version> </dependency>
<dependency> <groupId>de.schegge</groupId> <artifactId>rest-markdown-maven-plugin</artifactId> <version>1.1.9</version> </dependency>