4.1.2  Komplexe XML-2-XML Transformationen

Der erfahrene XML-Entwickler schreibt schlanken, performanten und einfachen Code, den auch andere gut verstehen. Er meistert alle Bereiche der XML-Entwicklung und hat sich mit Publishing Standards befasst, wie Docbook, DITA und JATS. Er erstellt mittels XSL-FO und verschiedenen Seitenvorlagen schöne PDF Dokumente und hat auch schon in anderen Anwendungsbereichen geabeitet, wie bspw. im EDI Umfeld. Er beherrscht XML Datenbanken, wie Marklogic oder eXist und deren Abfragesprache XQuery.
Als Königsdisziplin stellen sich komplexe XML-2-XML Transformationen heraus. Insbesondere solche, die von einem relativ freien Inhaltsmodell auf ein restriktives Inhaltsmodell abbilden. Dabei können diese ausserhalb und auch innerhalb einer XML Datenbank ablaufen - oder über Webrequests verteilt statt finden.
Es hat sich bewährt solche komplexen Transformationen auf mehrere Schritte aufzuteilen, die jeder für sich genommen, eine abgeschlossene und leicht zu testende Einheit bildet.
Schritt-für-Schritt wird dabei die XML Eingabeinstanz transformiert, bis schliesslich das validierbare Ergebnis herauskommt. Das XML der Zwischenschritte kann dabei meistens nicht gegen ein Schema validiert werden, weshalb eine besondere Sorgfalt bei der Entwicklung erforderlich ist.

4.1.2.1  Schritt-für-Schritt Python Skript

Bei einer mehrstufigen Transformation möchte man bei der Entwicklung leicht die Zwischenschritte überprüfen können. Dabei hilft eine andere Skriptsprache, wie bspw. Python. Das folgende Skript nimmt die XML Daten in einem Ordner input , transformiert diese in Sortierreihenfolge mit den XSLT Skripten, die im Ordner xslt liegen und schreibt die Ausgabe übersichtlich in den Ordner output .
import glob, os, shutil, getopt, sys, subprocess

SAXON_JAR = "/mnt/c/saxon/saxon9pe_98.jar"
JAVA_CMD = "java"

def transform(): 
    for fpath in glob.glob('input/*'):
        file_name= os.path.basename(fpath)
        input_folder = os.path.dirname(os.path.realpath(fpath))
        input_file =  os.path.join(input_folder, file_name)
  
        steps = os.listdir("xslt")
        steps.sort()
        step = None
        
        for step_file in steps:
            if not step_file.startswith("step"): continue 
            step = step_file.split("_")[0]
            output_folder = input_folder.replace("input","output/"+step)
            current_step = os.path.join(output_folder, file_name)
            os.makedirs(output_folder,exist_ok=True)
            args = [
                JAVA_CMD,
                "-classpath",
                SAXON_JAR,
                "net.sf.saxon.Transform",
                "-s:"+input_file,
                "-o:"+current_step,
                "-xsl:xslt/"+step_file,
                "filename="+os.path.basename(input_file).replace(".xml","")
                ]
            try:
                subprocess.call(args)
            except:
                print ("ERROR: Could not transform file: "+fpath+" with: "+step_file)
            input_file = current_step
            print ("Transformed "+step+": "+fpath)
        
transform()
print ("Done")
Das Skript kann natürlich noch um weitere Funktionen erweitert werden, wie bspw. einer Validierung für den letzten Schritt oder einem Deltavergleich der Zwischenergebnisse mit denen des vorherigen Transformationslaufs.
Will man das Ganze noch weiter treiben, kann man auch eine BPMN Engine, wie Camunda ↗↗ verwenden (einen speziellen Task-Executor für Camunda, der genau für diese XML Zwecke gemacht wurde, findet man auch in meinem Github Repository ↗↗).

4.1.2.2  Patterns für wiederkehrende Schritte

Eine mehrstufige Transformation, die auf ein restriktives Inhaltsmodell abbildet, funktioniert vielleicht wie eine Goldschürfer-Pipeline, in der gesiebt und gerüttelt wird, bis das erwartete Ergebnis vorliegt.
Folgende Patterns für wiederkehrende Schritte lassen sich dabei identifizieren:

4.1.2.3  Elemente markieren

Wenn man alles auf einmal transformieren will, kommt man schnell in Bedrängnis. Es empfiehlt sich zunächst zu markieren und im nächsten Schritt dann auf den markierten Elementen bestimmte Operationen auszuführen.
<xsl:template match="*[name()=$outline-element-names]">
  <xsl:copy>
    <xsl:apply-templates select="@*"/>
    <xsl:if test="preceding-sibling::*[1][@otherprops=$list-fragment-marker]">
      <xsl:attribute name="copy-target-id" 
                     select="preceding-sibling::*[@otherprops=$list-fragment-marker][1]/
                             descendant::*[@copy-id][last()]/@copy-id"/>
    </xsl:if>
    <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
</xsl:template>
Hier wird ein künstliches Attribut @copy-target-id mit einem Wert von @copy-id an ein Outline Element gesetzt, das im folgenden Schritt an die Stelle nach der @copy-id kopiert wird.

4.1.2.4  Elemente kopieren

Ein wiederverwendbarer Schritt, der mit @copy-target-id markierte Elemente nach eine Stelle kopiert, die mit @copy-id markiert wurde, könnte z.B. so aussehen:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs"
    version="2.0">
    
    <xsl:key name="targets" match="*[@copy-target-id]" use="@copy-target-id"/>
    
    <!-- copy elements from src to target -->
    
    <xsl:template match="*[@copy-id]">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
            <xsl:apply-templates select="key('targets',@copy-id)" mode="copied"/>
        </xsl:copy>
    </xsl:template>

    <!-- remove original position and attributes -->
	
    <xsl:template match="*[@copy-target-id]"/>
    <xsl:template match="@copy-target-id|@copy-id" mode="copied"/>
   
    <xsl:template match="node()|@*" mode="#all">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*" mode="#current"/>
        </xsl:copy>
    </xsl:template>
 
</xsl:stylesheet>
Mit so einer Vorgehensweise kann man sukkzessive und mittels einzelner Kopierschritte die XML Instanz umbauen und die Zwischenergebnisse verfolgen. So eine explorative Herangehensweise hat enorme Vorteile, wenn man sich über den Algorithmus noch nicht ganz im Klaren ist.

4.1.2.5  Elemente nach oben ziehen

Falls ein tieferliegendes Element nicht an die Stelle in der Ziel-DTD passt, kann man es mit folgenden Templates "nach oben ziehen":
<xsl:template match="table[descendant::table or descendant::ol]">
  <xsl:copy>
    <xsl:apply-templates mode="remove-table-ol"/>
  </xsl:copy>
  <xsl:apply-templates select="descendant::ol | descendant::table"/>
</xsl:template>
    
<xsl:template match="*[self::ol or self::table]" mode="remove-table-ol"/>
Hier werden Tabellen und Listen in einer Tabelle nach der Tabelle gesetzt. Über einen Modus (vgl. auch ein Beispiel zum Modus hier: Modus vs. Tunnel Lösung werden diese Knoten aus dem XML Zielbaum "ausgeschnitten".

4.1.2.6  Blöcke auszeichnen

Falls Blockstrukturen geklammert werden sollen, bspw. wenn diese im HTML Kapitel nur mittels h1 Überschriften-Tags gekennzeichnet sind, dann hilft vielleicht ein Template wie dieses weiter:
<xsl:template match="body">       
  <xsl:for-each select="h1">
    <block>
      <title>
        <xsl:apply-templates />
      </title>
      <xsl:apply-templates select="following-sibling::*[not(self::h1)]
          [preceding-sibling::h1[1][generate-id()=current()/generate-id()]]"/>                
    </block>
  </xsl:for-each>
</xsl:template>
Mittels der XPath fn:is() Funktion liesse sich der generate-id() Vergleich sogar noch abkürzen.

4.1.2.7  Mixed Content wrappen

Sehr unangenehm ist sporadisch auftretender XML Mixed Content, z.B. zwischen Paras. Mit folgenden Templates lässt sich das handeln. Zuerst kann man den Mixed Content in einem vorhergehenden Schritt markieren und in einen künstlichen Para packen ...
<!-- wrap a p around PCDATA in li -->
    
<xsl:template match="text()[parent::li]">
  <p content="mixed">
    <xsl:value-of select="."/>
  </p>
</xsl:template>
... hier markiert mit @content="mixed" . Im folgenden Schritt werden dann die ursprünglichen Paras, die jetzt verschachtelt im künstlichen Para liegen wieder ausgepackt:
<xsl:variable name="inline-elements" select="('sub','sup','b','i','br','u')"/>

<xsl:template match="p[@content='mixed' and not(preceding-sibling::p[@content='mixed'])]">
  <xsl:variable name="first-p-id" select="(preceding-sibling::*[1]/generate-id(), 
                                                                   generate-id())[1]"/>
  <p>
    <xsl:copy-of select="preceding-sibling::*[name()=$inline-elements]"/>
      <xsl:apply-templates select="node()|following-sibling::*[(self::p[@content='mixed']
                           or name()=$inline-elements) and 
			   not(preceding-sibling::p[not(@content='mixed')][1]/
                           generate-id()!=$first-p-id)]" mode="unwrap"/>
   </p>
</xsl:template>
	
<xsl:template match="p[@content='mixed' and preceding-sibling::p[@content='mixed']]"/>
	
<xsl:template match="p" mode="unwrap">
  <xsl:apply-templates/>
</xsl:template>
	
<xsl:template match="*[not(self::p)]" mode="unwrap">
  <xsl:copy>
    <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
</xsl:template>
	
<xsl:template match="li[p[@content='mixed']]/*[name()=$inline-elements]"/>
Previous Page Next Page
Version: 93
Jan 25 2021