It is a dynamic error…

XProc 1.0 wurde vor vier Jahren am 11. Mai als W3C Recommendation verabschiedet. Vor knapp drei Jahren begann ich selbst mit XProc zu entwickeln. Seitdem verwende ich die Programmiersprache bei nahezu jedem neuen XML-Projekt und nicht zuletzt bildet sie auch die Grundlage für unser Open Source Framework transpect. Das Lernen von XProc ist jedoch keine Selbstverständlichkeit. Die Syntax ist umständlich, das Debuggen ist kompliziert und es funktioniert vieles nicht, wie man es vielleicht von XSLT gewohnt sein mag. Dieser Beitrag versteht sich daher als Anfang einer kleinen Serie, die beschreibt wie man sich mit XProc das Leben mit XProc leichter machen kann.

Eines der häufigsten Probleme beim Entwickeln mit XProc ist das Abfangen von Fehlern. Fährt eine XProc Pipeline gegen die Wand, muss man mindestens dem Nutzer einen Hinweis geben, was schiefgelaufen ist. Ein typisches Beispiel ist die Prüfung von XML-Daten. XProc implementiert von Haus aus Validierungen gegen Schemas in Form von RelaxNG, W3C Schema und Schematron. Dafür stehen die steps p:validate-with-relax-ng, p:validate-with-xml-schema und p:validate-with-schematron zur Verfügung. Versieht man die steps mit dem Attribut assert-valid="true", wird im Falle invalider XML-Daten die Prozessierung der Pipeline abgebrochen. Nehmen wir als einfaches Beispiel eine XHTML-Datei, in der ein Element enthalten ist, welches im XHTML-Schema nicht vorgesehen ist.

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Titel</title>
  </head>
  <body>
    <p>
      <hurz>Der Wolf, das Lamm, ein Lurch lugt hervor…</hurz> 
    </p>
  </body>
</html>

Die folgende XProc-Pipeline lädt diese XHTML-Datei über ihren primären Input-Port und validiert diese gegen ein RelaxNG-Schema. Bei p:validate-with-relax-ng wird über den primären Input-Port source das zu validierende Dokument geladen. Der Input-Port wird in dem Beispiel nicht ausdrücklich angegeben, da der primäre Input-Port der Pipeline mit dem primären Input-Port des ersten Schrittes verbunden ist. Bei XProc werden auch zwischen Schritten implizit primäre Input- und Output-Ports miteinander verknüpft, sofern nicht explizit eine andere Verbindung deklariert wurde. Über den sekundären Input-Port schema wird das RelaxNG-Schema geladen, mit dem die XHTML-Datei schließlich validiert wird.

<?xml version="1.0" encoding="UTF-8"?>
<p:declare-step 
  xmlns:p="http://www.w3.org/ns/xproc" 
  version="1.0">

  <p:input port="source">
    <p:document href="test.xhtml"/>
  </p:input>
  
  <p:output port="result"/>
  
    <p:validate-with-relax-ng name="validate" assert-valid="true">
      <p:input port="schema">
        <p:document href="xhtml.rng"/>
      </p:input>
    </p:validate-with-relax-ng>
  
</p:declare-step>

Da hier das Attribut assert-valid den Wert true trägt, führt ein Valdierungsfehler zum unmittelbaren Abbruch der Pipeline. Das hat zwar den Vorteil, dass invalide Daten nicht einfach weiterverarbeitet werden, allerdings auch den Nachteil, dass der Prozess als Ganzes nicht weiter fortgesetzt wird. Stattdessen quittiert der XProc-Prozessor mit einer wenig aussagekräftigen Fehlermeldung den Dienst.

Fehler:/html/body[1]/p[1]/hurz[1]
SEVERE: err:XC0053:XC0053
SEVERE: It is a dynamic error if the assert-valid option is true and 
the input document is not valid.

Gegenüber dieser drakonischen Fehlerbehandlung wäre es wünschenswert, den Fehler zunächst abzufangen, um die Fehlermeldung später dem Nutzer in einer geeigneteren Form als einer Terminal-Ausgabe zu präsentieren. Wie in anderen Programmiersprachen gibt es auch in XProc zu diesem Zweck eine try-catch-Anweisung. Schlägt eine Operation im try-Zweig fehl, wird die Anweisung im catch-Zweig ausgeführt, ohne dass die Pipeline abbricht. In dem Beispiel weiter unten ist die Validierung in ein try-Element gekapselt. try hat als Kindelemente immer ein group-Element in dem der eigentliche Code ausgeführt wird und ein catch-Element, welches im Falle eines Fehlers prozessiert wird.

<?xml version="1.0" encoding="UTF-8"?>
<p:declare-step 
  xmlns:p="http://www.w3.org/ns/xproc" 
  version="1.0">
  
  <p:input port="source">
    <p:document href="test.xhtml"/>
  </p:input>
  
  <p:output port="result"/>
  
  <p:try name="try">
    <p:group>
      <p:output port="result">
        <p:pipe port="result" step="validate"/>
      </p:output>
      
      <p:validate-with-relax-ng name="validate" assert-valid="true">
        <p:input port="schema">
          <p:document href="xhtml/xhtml-strict.rng"/>
        </p:input>
      </p:validate-with-relax-ng>
      
    </p:group>
    
    <p:catch name="catch">
      <p:output port="result">
        <p:pipe port="result" step="copy-errors"/>
      </p:output>
      
      <p:identity name="copy-errors">
        <p:input port="source">
          <p:pipe step="catch" port="error"/>
        </p:input>
      </p:identity>
    </p:catch>
  </p:try>
  
</p:declare-step>

Wenn man try verwendet, muss darauf Wert gelegt werden, dass innerhalb von group und catch explizit die gleichen Ports deklariert werden. Die XProc-Spezifikation untersagt für konditionale Schritte die Deklaration unterschiedlicher Ports, da ansonsten je nach Eintreten einer Bedingung ein Schritt im laufenden Prozess unterschiedliche Ports ausweisen könnte.

Im catch-Zweig könnte nun beliebiger XProc-Code stehen, der alternativ ausgeführt wird. Jedoch bringt catch von Haus aus einen Output-Port namens error mit. Dieser fängt die Fehlermeldungen ab, welche beim Ausführen des try-Zweigs entstehen können. Auf diese Weise werden in dem Beispiel oben die Fehlermeldungen bei der Schema-Validierung abgefangen und verarbeitet. Dafür verwenden wir ein einfaches identity, welches die Ausgabe des error-Ports repliziert. Das wird über den Output-Port in catch verbunden und weitergereicht. In Falle der invaliden XHTML-Datei würde die XProc-Pipeline ein XML-Dokument produzieren, welches die Fehlerbeschreibung enthält.

<?xml version="1.0" encoding="UTF-8"?>
<c:errors xmlns:c="http://www.w3.org/ns/xproc-step">
  <c:error column="13" line="7">
    org.xml.sax.SAXParseException; systemId: test.xhtml; lineNumber: 7; columnNumber: 13; element "hurz" not allowed anywhere; expected the element end-tag, text or element "a", "abbr", "acronym", "applet", "b", "bdo", "big", "br", "button", "cite", "code", "del", "dfn", "em", "i", "iframe", "img", "input", "ins", "kbd", "label", "map", "noscript", "object", "q", "samp", "script", "select", "small", "span", "strong", "sub", "sup", "textarea", "tt" or "var"
  </c:error>
</c:errors>

Da die Fehlermeldungen als XML ausgegeben werden, können diese in der XProc-Pipeline unmittelbar von nachfolgenden Schritten weiterverarbeitet werden. Dergestalt kann man die aufgetretenen Fehler sammeln und dem Nutzer in Form eines PDF- oder HTML-Berichts ausgeben. Zudem können die Resultate von Teilprozessen trotzdem ausgegeben werden, sofern sie nicht von den Ergebnissen des fehlgeschlagenen Prozesses abhängig sind.

Software kann immer Fehler enthalten. Gerade im Verlagsbereich hat man es als Entwickler häufig mit Daten zu tun, die je nach Quelle unvorhergesehene Strukturen mit sich bringen können. Es ist unmöglich alle Dinge, die bei der Erstellung von Inhalten machbar sind, zu antezipieren. Eine gute Fehlerbehandlung kann aber helfen, dass man von den Anwendern bessere Fehlermeldungen erhält, was letztlich wieder der kontinuierlichen Weiterentwicklung der Anwendung zu Gute kommt. Zudem ist es für die Nutzer transparenter, wenn sie einen Fehlerbericht erhalten, als dass sich die Anwendung sang- und klanglos verabschiedet. Ein Fehlerbericht ist nicht selten besser als gar keiner.

Kommentar schreiben