Die XSLT-Story

Stellt euch vor, ihr habt ein WordPress-Plugin geschrieben, das eine XML-Datei aufarbeitet um dessen Inhalt für den Betrachter sinnvoll lesbar auf einer Webseite darstellt. Nachdem mit den Testdaten alles wunderbar funktioniert spielst du Echtdaten ein und schaltest das System produktiv. Es funktioniert tatsächlich noch immer wunderbar, aber dir fällt doch auf, das da sehr viel unnötiges in der gelieferten XML-Datei steht, da in diese tatsächlich jedes vorhandene und theoretisch füllbare Feld exportiert wird.
Beispiel: Es geht um Kurse. Die könnten ja zu jedem Termin an einem anderen Ort stattfinden. Macht viel Sinn, wenn das so ist. Bei uns ist es nicht so, und daher sind diese Daten schon mal überflüssig.
Noch ein Beispiel: Die Kontaktdaten der Dozenten. Das ist nicht nur kosmetischer Ballast. Die möchte ich tatsächlich in keinster Weise online stellen. Daten, die erst gar nicht da sind, sind dann auch in keinster Weise missbräuchlich auszulesen. Old School Security. Und ganz nebenbei wird die Daten liefernde Datei wieder etwas schmaler. Denn diese hat mit einigen MB doch einen überraschenden Umfang erreicht.
Nun kommst du in deinem jugendlichen Datenleichtsinn auf die Idee, das doch einer der unzähligen XML-Editoren in der Lage sein könnte solche überflüssigen Knoten konsequent aus der Datei zu entfernen. Diese Hoffnung musste ich nach einer längeren Sitzung mit einer bekannten Suchmaschine und der Installation einiger Anwendungen jedoch tatsächlich aufgeben. Zu meiner Verwirrung, wie ich noch immer feststellen muss. Denn für so besonders halte ich meinen Wunsch nun wirklich nicht. Aber es ist wie es ist. Also musste ich ein noch intensiveres Gespräch mit der bekannten Suchmaschine führen. Apropos Gespräch – Ob Siri, Cortana & Co mir schon brauchbare Antworten auf meine Frage geliefert hätten? „Hey Alexa, ich möchte wiederkehrende Knoten aus einer XML-Datei entfernen, wie mache ich das am besten?“
Ich hab es – ganz Old Style – mit klassischem eintippen versucht und bin zuerst durch einen Dschungel an Lösungen für so ziemlich jede Programmiersprache dieses Planeten geirrt. Um dann aber der Tatsache bewusst zu werden, das XML mit XSLT das nötige Werkzeug tatsächlich mehr oder weniger mitliefert.

Inzwischen hatte die bekannte Suchmaschine wohl begriffen, das es mir erst war und lieferte mir folgenden Link:
http://www.microhowto.info/howto/process_an_xml_document_using_an_xslt_stylesheet.html

Process an xml document using an xslt stylesheet – Was für ein schöner Satz!

Das Tutorial beginnt mit:

Scenario

Suppose that you have an XML document called input.xml that you wish to process using an XSLT stylesheet called style.xsl to produce a new XML document called output.xml.

Inzwischen hatte sich auch mein Ansatz entsprechend ein wenig verschoben. Wenn ich aus einer Datei geschätzte 90% der hinterlegten Informationen eliminieren will, dann macht es wahrscheinlich mehr Sinn, die zu verbleibenden 10% zu definieren.
Der Blick in ein XSL-Tutorial offenbart geradezu erschreckendes. Ist das Einfach! Eine Schleife und ein paar Zeilen Fleißarbeit. Das Stylesheet ist wesentlich schneller geschrieben als die Suche nach der Methode bis hier gedauert hat.

Das hier ist schon alles.

<?xml version="1.0" encoding="windows-1252"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/kurse">
    <kurse>
        <xsl:for-each select="kurs">
            <kurs>
                <knr>
                    <xsl:value-of select="knr"/>
                </knr>
                <fachb>
                    <xsl:value-of select="fachb"/>
                </fachb>
                <fachbtext>
                    <xsl:value-of select="fachbtext"/>
                </fachbtext>
                <haupttitel>
                    <xsl:value-of select="haupttitel"/>
                </haupttitel>
                <inhalt>
                    <xsl:value-of select="inhalt"/>
                </inhalt>
                <mitarbeiter_planend>
                    <xsl:value-of select="mitarbeiter_planend"/>
                </mitarbeiter_planend>
                <ort>
                    <xsl:value-of select="ort"/>
                </ort>
                <ortaussenstelle>
                    <xsl:value-of select="ortaussenstelle"/>
                </ortaussenstelle>
                <ortraumname>
                    <xsl:value-of select="ortraumname"/>
                </ortraumname>
                <ortgebaeude>
                    <xsl:value-of select="ortgebaeude"/>
                </ortgebaeude>
                <ortstr>
                    <xsl:value-of select="ortstr"/>
                </ortstr>
                <ortplz>
                    <xsl:value-of select="ortplz"/>
                </ortplz>
                <ortname>
                    <xsl:value-of select="ortname"/>
                </ortname>
                <beginndat>
                    <xsl:value-of select="beginndat"/>
                </beginndat>
                <endedat>
                    <xsl:value-of select="endedat"/>
                </endedat>
                <beginnuhr>
                    <xsl:value-of select="beginnuhr"/>
                </beginnuhr>
                <endeuhr>
                    <xsl:value-of select="endeuhr"/>
                </endeuhr>
                <dauer>
                    <xsl:value-of select="dauer"/>
                </dauer>
                <termine>
                    <xsl:for-each select="termine/termin">
                        <termin>
                        <tag>
                            <xsl:value-of select="tag"/>
                        </tag>
                        <zeitvon>
                            <xsl:value-of select="zeitvon"/>
                        </zeitvon>
                        <zeitbis>
                            <xsl:value-of select="zeitbis"/>
                        </zeitbis>
                        </termin>
                    </xsl:for-each>
                </termine>
                <tnmax>
                    <xsl:value-of select="tnmax"/>
                </tnmax>
                <tnmin>
                    <xsl:value-of select="tnmin"/>
                </tnmin>
                <tnanmeldungen>
                    <xsl:value-of select="tnanmeldungen"/>
                </tnanmeldungen>
            </kurs>
        </xsl:for-each>
    </kurse>
</xsl:template>
</xsl:stylesheet>

Apple liefert den nun noch benötigten Processor als Kommandozeilentool in OS X freundlicherweise (noch) mit. Ich bin trotzigerweise noch bei 10.8.5, und dort ist xsltproc einfach so da.
Wa schön ist, denn damit kann man mit einem einfach Anufruf á la:

$ xsltproc -o output.xml style.xsl input.xml

zu dem von mir gewünschten Ergebnis kommen.

Problem solved!

Statt langer Suche, hätte möglicherweise bewusstes lesen der Informationen, die jemand in der Wikipedia hat liegen lassen, direkt helfen können.
Dort steht im XML Eintrag zu Transformation und Darstellung von XML-Dokumenten:
„Ein XML-Dokument kann mittels geeigneter Transformationssprachen wie XSLT oder DSSSL in ein anderes Dokument transformiert werden.“
Und dieses andere Dokument kann nun eben auch ein anderes XML-Dokument sein. Hier eines, das nur die individuell benötigten Knoten der Ursprungsdatei enthält.

KuferSQL WordPress-Plugin

Mein erstes WordPress-Plugin.

Obwohl ich kein begnadeter Programmierer bin, weiss ich mir jedoch meist zu helfen. Daher haben wir hier nicht unbedingt eine Lehrbuchhafte Programmierlösung, aber eine Funktionierende.
Die Kursverwaltung KuferSQL bietet unter dem Namen KuferWEB Lösungen um die angebotenen Kurse Online darzustellen. Technisch wird die aus der Anwendung exportierten Daten in Typo3 importiert und abgebildet (Replikat-Betrieb). Im sog. Hybrid-Betrieb gibt es einen kontinuierlichen Abgleich der M$SQL, welche der KuferSQL Anwendung zugrunde liegt und der MySQL-Datenbank für den Online-Auftritt. Und im Direkt-Betrieb greifen Online-Auftritt und Anwendung gar auf die gleiche Datenbank zu. Dies ist für unsere Zwecke tatsächlich ein bis zwei Nummern zu groß gedacht. So entstand der Wunsch nach einer kleineren Lösung. Diese besteht aus der Visualisierung des XML-Exports der darzustellenden Kurse auf einer WordPress-Seite. Dazu wird die auf KuferSQL exportierte internet.xml im Pluginverzeichnis hinterlegt. Die Auswertung kann auf jeder Seite mittels Shortcode [kufer] eingebunden werden. Das Ergebnis findet sich hier: http://ifak-bochum.de/bildungsangebote/

<?php
/*
Plugin Name: Kufer XML Parser
Plugin URI: http://ifak-bochum.de
Description: Ein Plugin zur Aufbereitung des XML Exports von KuferSQL.
Version: 0.1
Author: Rafael Häusler
Author URI: http://seinplanet.de
*/

add_action('wp_print_styles', 'add_my_styles', 100);
function add_my_styles() {
    wp_register_style( 'eigenes-css', 'http://www.ifak-bochum.de/wp-content/plugins/kufer/styles.css');
    wp_enqueue_style( 'eigenes-css' );
}
add_action('init', 'add_my_styles');

function kuferxml(){
    $kurse = simplexml_load_file('/homepages/31/d22266140/htdocs/ifak-wp-2014/wp-content/plugins/kufer/internet.xml');
    $result .= '<h3>Fachbereiche</h3><ul class="kufer-fachbereichsliste">';
    foreach ($kurse as $kurs):
        $knr=(string) $kurs->knr;
        $fachb=(string) $kurs->fachb;
        $fachbtext=(string) $kurs->fachbtext;
        $fachbereiche[$fachb] = $fachbtext;
    endforeach;
    foreach ($fachbereiche as $fbkey => $fbvalue) {
        if($_GET['fb'] == $fbkey) {
            $fbselected = 'class="fbselected"';
        }    
        else {
            unset($fbselected);
        }
        $result .= '<li '.$fbselected.'><a href="?fb='.$fbkey.'">'.$fbvalue.'</a></li>';        
    }
    $result .= '</ul><br style="clear:both"><hr>';    
    if($_GET['knr']) {
        foreach ($kurse as $kurs):
            $knr=$kurs->knr;
            $fachb=$kurs->fachb;
            $titel=$kurs->haupttitel;
            $inhalt=$kurs->inhalt;
            $mitarbeiter_planend=$kurs->mitarbeiter_planend;
            $ort=$kurs->ort;
            $ortaussenstelle=$kurs->ortaussenstelle;
            $ortraumname=$kurs->ortraumname;
            $ortgebaeude=$kurs->ortgebaeude;
            $ortstr=$kurs->ortstr;
            $ortplz=$kurs->ortplz;
            $ortname=$kurs->ortname;
            $fachbtext=$kurs->fachbtext;
            $beginndat=$kurs->beginndat;
            $endedat=$kurs->endedat;
            $beginnuhr=$kurs->beginnuhr;
            $endeuhr=$kurs->endeuhr;
            $dauer=$kurs->dauer;
            $termine=$kurs->termine->termin;
            $dozenten=$kurs->dozenten->dozent;
            $tnmax=$kurs->tnmax;
            $tnmin=$kurs->tnmin;
            $tnanmeldungen=$kurs->tnanmeldungen;                
            $full=$tnmax-$tnanmeldungen;
            if($_GET['knr'] == $knr) {
                $result .= '<h3>'.$titel.'</h3>';
                $result .= '<div class="block">';
                $result .= '<table class="kursdetails" border="0">';
                $result .= '<tr>';
                $result .= '<td>Kursnummer</td><td>'.$knr.'</td>';
                $result .= '</tr><tr>';
                $result .= '<td>Zeitraum</td><td>'.$beginndat.' - '.$endedat.'</td>';
                $result .= '</tr><tr>';
                $result .= '<td>Uhrzeit</td><td>'.$beginnuhr.' - '.$endeuhr.' Uhr</td>';
                $result .= '</tr><tr>';
                $result .= '<td>Dauer</td><td>'.$dauer.' x</td>';
                $result .= '</tr><tr>';
                $result .= '<td>Kursort</td><td>'.$ortgebaeude.'<br>'.$ortstr.', '.$ortplz.' '.$ortname.'</td>';
                $result .= '</tr>';
                $result .= '</table>';                
                $result .= '<div class="kufer-row kw-table-header"><div class="column">Datum</div><div class="column">Uhrzeit</div></div>';
                $result .= '<div class="kursterminliste">';
                foreach ($termine as $termin):
                    $tag=$termin->tag;
                    $zeitvon=$termin->zeitvon;
                    $zeitbis=$termin->zeitbis;
                    $termin_ortraumname=$termin->termin_ortraumname;
                    $termin_ortgebaeude=$termin->termin_ortgebaeude;
                    $termin_ortstr=$termin->termin_ortstr;
                    $termin_ortplz=$termin->termin_ortplz;
                    $termin_ortname=$termin->termin_ortname;
                    $result .= '<div class="kufer-row kw-table-row">';
                    $result .= '<div class="column kw-table-data">'.$tag.'</div>';
                    $result .= '<div class="column kw-table-data">'.$zeitvon.' - '.$zeitbis.' Uhr</div>';
                    $result .= '</div>';
                endforeach;
                $result .= '</div>';                
                $result .= '</div>';
                $result .= '<div class="block">';
                $result .= $inhalt;
                $result .= '</div>';
                if($full<3) $ampel = '<div class="block"><br><br>Es sind nur noch wenige Plätze frei!</div>';
                if($full<1) $ampel = '<div class="block"><br><br>Dieser Kurs ist leider bereits ausgebucht.</div>';
                $result .= $ampel;
                if($mitarbeiter_planend=="BN") $result .= '<div class="block"><br><br>Bei Interesse an diesem Kurs senden Sie uns eine Nachricht an <a href="mailto:anmeldung@ifak-bochum.de?subject=Interesse an Kurs: '.$titel.' ('.$knr.')">anmeldung@ifak-bochum.de</a> oder rufen Sie uns unter 0234 - 962 10 22 an.</div>';
                if($mitarbeiter_planend=="IK") $result .= '<div class="block"><br><br>Bei Interesse an diesem Kurs senden Sie uns eine Nachricht an <a href="mailto:sprachfoerderung@ifak-bochum.de?subject=Interesse an Kurs: '.$titel.' ('.$knr.')">sprachfoerderung@ifak-bochum.de</a> oder rufen Sie uns unter 0234 - 92 33 62 39 an.</div>';
                $result .= '<br style="clear:both">';
            }                        
        endforeach;                
    }
    else {
        if($_GET['fb']) $result .= '<h3>Kurse</h3><ul class="kufer-kursliste">';
        foreach ($kurse as $kurs):
            $fachb=(string) $kurs->fachb;
            if($_GET['fb'] == $fachb) {
                $knr=$kurs->knr;
                $fachb=(string) $kurs->fachb;
                $titel=$kurs->haupttitel;
                $inhalt=$kurs->inhalt;
                $ortaussenstelle=$kurs->ortaussenstelle;
                $fachbtext=$kurs->fachbtext;
                $beginndat=$kurs->beginndat;
                $endedat=$kurs->endedat;
                $beginnuhr=$kurs->beginnuhr;
                $endeuhr=$kurs->endeuhr;
                $dauer=$kurs->dauer;
                $tnmax=$kurs->tnmax;
                $tnmin=$kurs->tnmin;
                $tnanmeldungen=$kurs->tnanmeldungen;                
                $full=$tnmax-$tnanmeldungen;
                $fachbereiche[] = $fachbtext;
                $ampel='<div class="kufer-green">&nbsp;</div>';
                if($full<3) $ampel='<div class="kufer-yellow">&nbsp;</div>';
                if($full<1) $ampel='<div class="kufer-red">&nbsp;</div>';
                $result .= '<li><div style="float: left;width: 400px;"><a href="?knr='.$knr.'&fb='.$fachb.'">'.substr($knr, 3,4).' '.$titel.'</a></div><div style="float: left;width: 200px;">'.$beginndat.' - '.$endedat.'</div><div style="float: left;width: 150px;">'.$dauer.' Termine</div><div style="float: left;width: 250px;">'.$ortaussenstelle.'</div>'.$ampel.'<div style="clear:left"> </div></li>';                
            }
        endforeach;
    }
    return $result;
}
add_shortcode('kufer','kuferxml');
?>

Es ist nicht geplant das Plugin ausführlich zu Dokumentieren. Jedwelche Einstellungen finden sich in der Plugin-und der korrospondierenden CSS-Datei direkt.
Da wir aber vielleicht nicht die einzige Einrichtung sind, die auf eine Typo3-Seite verzichten wollen und den von KuferWEB gebotenen Funktionsumfang gar nicht benötigen, habe ich mich entschieden den Quelltext des Plugins an dieser Stelle öffentlich zugänglich zu machen.