Wenn man mit Python programmiert, hat man leider nicht Saxon als XSLT Prozessor zur verfügung, sondern muss sich mit dem XSLT Prozessor der lxml Bibliothek begnügen. Diese basiert auf der C Bibliothek libxslt
Dabei steht nur XSLT 1.0 zur Verfügung mit den EXSLT Erweiterungen.
Unter diesen Bedingungen war es nicht ganz einfach eine Transformation zu erstellen, die XML als Eingabe nimmt und sauberes JSON als Ausgabe produziert. Sicherlich ist hierfür ein mehrstufiger Prozess erforderlich.
Um genau zu sein braucht man mindestens eine Vortransformation, die die JSON Elemente als XML erzeugt und ein Postprocessing, das möglichst generisch die spitzen Klammern auf geschweifte abbildet und einige Elementnamen verwirft.
Die folgenden JSON Strukturen lassen sich leicht identifizieren:
1. Properties
"propertyName" : propertyValuePlain Text
1. Dabei kann der propertyValue vom Typ String, Number oder Boolean sein.
In Objekten können Properties, benannte Objekte oder benannte Listen enthalten sein. In benannten Listen können nur anonyme Objekte enthalten sein. Diese Unterscheidung hat erst einmal für meinen Anwendungsfall ausgereicht.
Umgemünzt auf XSLT Regeln, kommt man auf vier Regeln die anhand einer Attributbelegung für @json matchen:
<xsl:template match="*[@json='anonymous-object']" mode="post"> <xsl:text>{</xsl:text> <xsl:apply-templates select="node()|@*" mode="post"/> <xsl:text>}</xsl:text> <xsl:if test="following-sibling::*">,</xsl:if> </xsl:template> <xsl:template match="*[@json='named-list']" mode="post"> <xsl:text>"</xsl:text> <xsl:value-of select="name()"/> <xsl:text>"</xsl:text> <xsl:text>:[</xsl:text> <xsl:apply-templates select="node()|@*" mode="post"/> <xsl:text>]</xsl:text> <xsl:if test="following-sibling::*">,</xsl:if> </xsl:template> <xsl:template match="*[@json='named-object']" mode="post"> <xsl:text>"</xsl:text> <xsl:value-of select="name()"/> <xsl:text>"</xsl:text> <xsl:text>:{</xsl:text> <xsl:apply-templates select="node()|@*" mode="post"/> <xsl:text>}</xsl:text> <xsl:if test="following-sibling::*">,</xsl:if> </xsl:template> <xsl:template match="*[@json='property']" mode="post"> <xsl:text>"</xsl:text> <xsl:value-of select="name()"/> <xsl:choose> <xsl:when test="@type='boolean' or @type='integer'"> <xsl:text>":</xsl:text> <xsl:value-of select="."/> </xsl:when> <xsl:otherwise> <xsl:text>":"</xsl:text> <xsl:value-of select="."/> <xsl:text>"</xsl:text> </xsl:otherwise> </xsl:choose> <xsl:if test="following-sibling::*">,</xsl:if> </xsl:template>Plain Text
In einer Vortransformation in eine Variable pre werden die JSON Strukturen erstmal als XML aufgebaut und dann in einem Postprocessing Step post auf die geschweiften Klammern gemappt:
<xsl:template match="/"> <root> <!-- first processing step --> <xsl:variable name="pre"> <xsl:copy> <xsl:apply-templates/> </xsl:copy> </xsl:variable> <!-- second processing step --> <xsl:apply-templates select="exsl:node-set($pre)" mode="post"/> </root> </xsl:template>Plain Text
Das Aufbauen der XML-artigen JSON Struktur mit den annotierten @json Attributen, könnte z.B. so aussehen:
<xsl:template match="testsuite"> <sarif-report json="anonymous-object"> <runs json="named-list"> <run json="anonymous-object"> [...] </run> </runs> <version json="property">2.1.0</version> </sarif-report> </xsl:template>Plain Text
Mit folgender finaler Ausgabe:
{ "runs" : [{ ... }],
"version": "2.1.0"
}Plain TextDa Python lxml bzw. libxslt anscheinend kein xsl:output method="text" beherscht, muss im Python Code der Textknoten eines Root-Elements ausgelesen werden. Wenn man diesen als JSON in ein Python dictionary liest und anschliessend wieder als JSON mit Indention herausschreibt, hat man auch gleich ein schönes Pretty-Printing der JSON Strukturen. Der relevante Python Code sieht dabei so aus:
with fileobj: # We register namespace functions that can be called inside the XSLT transformation register_stylesheet_functions() # Read the XSLT stylesheet xsl = etree.XML(open(SARIF_TRANSFORMATION_FILE, "r", encoding="utf-8").read()) # Transform the XML to Sarif JSON and put it into the root text node transform = etree.XSLT(xsl) result = transform(tree) if INDENT: # Load into python dict and pretty print when writing back to JSON jsn = json.loads(result.getroot().text) fileobj.write(json.dumps(jsn, indent=2)) else: fileobj.write(result.getroot().text)Plain Text
Das vollständige Beispiel findet man in meinem Github Repo.