„The three great virtues of a programmer: laziness, impatience, and hubris.“
Larry Wall
Fehlerbehandlung ist ein zentrales Thema bei einer Template-Engine. Fehlerhafte Eingabedaten und Syntaxfehler im Template sollten schnell erkannt werden, damit der Entwickler nicht lange Zeit mit der Fehlersuche beschäftigt ist. Bei der Verwendung von FreshMarker treten Fehler in drei Phasen der Verwendung auf. Bei der Konfiguration der Template-Engine, dem Parsen der Template Vorlagen und beim Prozessieren des Templates.
Fehler während der Konfiguration der Template-Engine resultieren normalerweise aus Problemen mit den Erweiterungen oder den Resourcen, auf die zugegriffen werden soll. . Beim Parsen der Template Vorlagen treten IOExceptions
und ParseExceptions
auf. Erstere wenn die Template Vorlagen nicht gefunden werden oder Fehler beim Einlesen auftreten. Letztere werden vom JavaCC 21 Parser geworfen, wenn die Vorlagen nicht der FreshMarker Syntax genügen. Die JavaCC 21 ParseException
enthalten dabei einen Verweis auf die Fehlerposition in der Eingabe.
Während der Prozessierung des Templates können Fehler durch die verwendeten Eingabedaten auftreten. Üblicherweise passen die Daten in den Variablen nicht zu den Built-Ins oder den Ausdrücken in den Interpolations.
Das folgende Beispiel zeigt ein kurzes Template, das einen Text der ein A enthält in Großbuchstaben und einen Text der ein B enthält in Kleinbuchstaben enthält. Andere Texte werden unverändert dargestellt.
<#if text?contains('A')> ${text?upper_case} <#elseif text?contains('B')> ${text?lower_case} <#else> ${text} </#if>
Das Template ist syntaktisch korrekt, daher treten keine Fehler während des Parsens auf. Während der Prozessierung des Templates kann es zu einem Fehler kommen, wenn die Variable text
keinen Text, sondern eine Zahl enthält. Der folgende Unit Test prüft genau diese Situation.
@Test void ifConditionError() throws IOException, ParseException { Template template = configuration.getTemplate("test"); Map<String, Object> dataModel = Map.of("text", 42); UnsupportedBuiltInException exception = assertThrows(UnsupportedBuiltInException.class, () -> template.process(dataModel)); assertEquals("unsupported builtin 'contains' for TemplateNumber at input:1:6 'text?contains('A')'", exception.getMessage()); }
Während der Auswertung der Interpolation wird eine UnsupportedBuiltInException
geworfen, weil es kein Built-In contains
für den Type TemplateNumber
gibt. Unglücklichweise steht an dieser Position nicht der vollständige fehlerhafte Ausdruck bereit sondern nur das Built-In contains('A')
. Daher wird die UnsupportedBuiltInException
nur mit der Message unsupported builtin 'contains' for TemplateNumber
erzeugt und geworfen. Damit der vollständige Text der Interpolation angezeigt werden kann, wird das Exception-Handling genutzt.
public class IfFragment implements Fragment { private final List<ConditionalFragment> fragments = new ArrayList<>(); public void addFragment(ConditionalFragment fragment) { fragments.add(fragment); } @Override public void process(ProcessContext context) { fragments.stream().filter(f -> filterByConditional(context, f)) .findFirst().ifPresent(f -> f.process(context)); } private boolean filterByConditional(ProcessContext context, ConditionalFragment conditionalFragment) { try { return conditionalFragment.getConditional().evaluate(context, TemplateBoolean.class) == TemplateBoolean.TRUE; } catch (UnsupportedBuiltInException e) { throw new UnsupportedBuiltInException(e.getMessage(), conditionalFragment.getNode(), e); } catch (WrongTypeException e) { throw new WrongTypeException(e.getMessage(), conditionalFragment.getNode(), e); } } }
Die UnsupportedBuiltInException
wird innerhalb der Fragmente gefangen und durch eine entsprechende Variante ersetzt, die mit dem passenden JavaCC 21 Node instanziiert wurden. In diesem Fall ist es das IfFragment
für die Prozessierung der If/ElseIf/Else Direktive. Andere Direktiven, die UnsupportedBuiltInException
fangen sind das InterpolationFragment
für die Interpolations und das SwitchFragment
für die Switch Direktive.
Damit die Positionen und der Inhalt eines Ausdrucks ausgegeben werden können, produzieren die Subklassen von ProcessException
ihre Fehlermeldungen mit Hilfe der JavaCC 21 Node Instanzen.
public ProcessException(String message, Node node, Throwable cause) { super(message + " at " + generateLocation(node), cause); } private static String generateLocation(Node node) { return node.getLocation() + " '" + node.getSource() + "'"; }
Die Methode getLocation
liefert einen String
aus dem Namen der Eingabe und der Anfangsposition in der Eingabe und getSource
liefert die Textdarstellung des Knoten aus der Eingabe. Im hier verwendeten Beispiel die folgende Meldung.
unsupported builtin 'contains' for TemplateNumber at input:1:6 'text?contains('A')'
Tritt nun ein Fehler bei der Bearbeitung des Templates auf, dann kann der Nutzer das Problem recht schnell eingrenzen.