FreshMarker Fehlerbehandlung

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.

Leave a Comment