Ein- und Ausgabe

(Auszug aus "Java und XSLT" von Eric M. Burke)

   

   

Wie andere XML-Tools können XSLT-Prozessoren ihre Eingabedaten aus vielen verschiedenen Quellen lesen. Im grundlegendsten Szenario wird ein statisches Stylesheet und ein XML-Dokument über die Klasse java.io.File geladen. Geläufiger ist jedoch, daß das XSLT-Stylesheet aus einer Datei kommt, während die XML-Daten als Ergebnis einer Datenbankabfrage dynamisch erzeugt werden. In diesem Fall macht es keinen Sinn, die Datenbank-Abfrageergebnisse erst in eine XML-Datei zu schreiben und dann in den XSLT-Prozessor zu parsen. Statt dessen wird man die XML-Daten über SAX oder DOM besser direkt in den Prozessor leiten. Tatsächlich werden wir auch sehen, wie man Nicht-XML-Daten einliest und diese mit XSLT transformiert.

System-Identifier, Dateien und URLs

Die an früherer Stelle in diesem Kapitel gezeigten, simplen Beispiele haben das Konzept eines System-Identifikators eingeführt. Wie zuvor erwähnt, sind System-Identifier nichts anderes als URIs und werden häufig von XML-Tools genutzt. Eines der wichtigsten Interfaces von JAXP, javax.xml.transform.Source, hat beispielsweise die folgende API:

public interface Source {
    String getSystemId( );
    void setSystemId(String systemId);
}

Die zweite Methode, setSystemId( ), ist entscheidend. Indem man dem Source-Objekt einen URI übergibt, kann der XSLT-Prozessor URIs in XSLT-Stylesheets auflösen. Daher funktioniert XSLT-Code wie der folgende:

<xsl:import href="allgemeineFusszeilen.xslt"/>

Wenn es an die XSLT-Programmierung geht, werden Sie Methoden aus java.io.File und java.net.URL verwenden, um plattformspezifische Dateinamen in System-IDs zu konvertieren. Diese können dann als Parameter für alle Methoden eingesetzt werden, die eine System-ID als Parameter erwarten. Der folgende Code dient beispielsweise der Konvertierung eines plattformspezifischen Dateinamens in eine System-ID:

public static void main(String[] args) {
 // es wird angenommen, daß der erste Kommandozeilenparameter
 // einen Dateinamen enthält
 // - unter Windows so ähnlich wie "C:\home\index.xml"
 // - unter Unix so ähnlich wie "/usr/home/index.xml"
 String dateiname = args[0];
 File fileObjekt = new File(dateiname);
 URL dateiURL = fileObjekt.toURL( );
 String systemID = dateiURL.toExternalForm( );

Dieser Code wurde zur Verdeutlichung in mehrere Zeilen aufgeteilt; er kann wie folgt zusammengefaßt werden:

String systemID = new File(dateiname).toURL().toExternalForm( );

Die Konvertierung von einem System-Identifikator zurück in einen Dateinamen oder ein File-Objekt kann folgendermaßen bewerkstelligt werden:

URL url = new URL(systemID);
String dateiname = url.getFile( );
File fileObjekt = new File(dateiname);

Und wieder einmal kann das zu einer einzigen Zeile zusammengefaßt werden:

File fileObjekt = new File((new URL(systemID)).getFile( ));

JAXP I/O Design

Die Interfaces Source und Result in javax.xml.transform bilden die Grundlagen für alle Transformations-Ein- und -Ausgaben in JAXP 1.1. Unabhängig davon, ob ein Stylesheet als URI, Dateiname oder InputStream vorliegt, werden seine Daten an JAXP über eine Implementierung des Source-Interface übertragen. Die Ausgabe wird dann an eine Implementierung des Result-Interface gesendet. Die von JAXP bereitgestellten Implementierungen sind in der folgenden Abbildung aufgeführt.

Die Interfaces Source und Result

Abbildung: Die Interfaces Source und Result

Wie Sie sehen können, ist JAXP nicht darauf festgelegt, woher es seine Daten bekommt und wohin es die Ergebnisse schickt. Erinnern Sie sich, daß zwei Instanzen von Source immer spezifiziert sind: eine für die XML-Daten und die andere für das XSLT-Stylesheet.

JAXP Stream I/O

Wie in obiger Abbildung gezeigt, ist StreamSource eine der Implementierungen des Source-Interface. Zusätzlich zu den System-Identifier, die Source bereitstellt, erlaubt StreamSource die Eingabe über ein File-Objekt, ein InputStream-Objekt oder ein Reader-Objekt. Die Klasse BeispielJaxp aus dem Beispiel BeispielJaxp.java hat gezeigt, wie man über StreamSource aus einem File-Objekt lesen kann. Es gibt darüber hinaus vier Konstruktoren, die ermöglichen, eine StreamSource-Instanz entweder von einem InputStream-Objekt oder einem Reader-Objekt zu erzeugen. Die vollständige Liste der Konstruktoren sehen Sie hier:

public StreamSource( )
public StreamSource(File f)
public StreamSource(String systemId)
public StreamSource(InputStream byteStream)
public StreamSource(InputStream byteStream, String systemId)
public StreamSource(Reader characterStream)
public StreamSource(Reader characterStream, String systemId)

Der erste Parameter der Konstruktoren, die InputStream und Reader als Parameter erwarten, sind entweder die XML-Daten oder das XSLT-Stylesheet. Der zweite Parameter wird, falls vorhanden, zur Auflösung relativer URI-Referenzen im Dokument verwendet. Wie zuvor erwähnt, kann Ihr XSLT-Stylesheet den folgenden Code enthalten:

<xsl:import href="allgemeineFusszeilen.xslt"/>

Übergibt man dem StreamSource-Objekt einen System-Identifikator als Parameter, teilt man dem XSLT-Prozessor mit, wo er nach allgemeineFusszeilen.xslt suchen soll. Ohne diesen Parameter könnte ein Fehler auftreten, wenn der Prozessor den URI nicht auflösen kann. Die Lösung liegt im Aufruf der Methode setSystemId( ):

// ein StreamSource-Objekt, das von einem InputStream liest, wird erzeugt
Source meineSource = new StreamSource(einInputStream);
// die System-ID (ein String) wird spezifiziert, damit das Source-Objekt
// relative URIs auflösen kann, auf die es in XSLT-Stylesheets stößt.
meineSource.setSystemId(eineSystemId);

Die Dokumentation für StreamSource empfiehlt, daß man InputStream dem Reader vorziehen soll, weil das dem Prozessor gestattet, die Zeichenkodierung korrekt zu handhaben, nämlich wie in der XML-Deklaration angegeben.

StreamResult ist in der Funktionalität StreamSource ähnlich, obwohl hier relative URIs nicht notwendigerweise aufgelöst werden müssen. Die verfügbaren Konstruktoren lauten wie folgt:

public StreamResult( )
public StreamResult(File f)
public StreamResult(String systemId)
public StreamResult(OutputStream byteStream)
public StreamResult(Writer characterStream)

Lassen Sie uns einige der anderen Optionen für StreamSource und StreamResult anschauen. Das folgende Beispiel ist eine Modifikation des BeispielJaxp-Programms, das zuvor gezeigt wurde. Es lädt die XML-Spezifikation von der W3C-Website herunter und speichert sie in einer temporären Datei auf Ihrer lokalen Festplatte. Um die Datei herunterzuladen, wird ein StreamSource-Objekt mit einem System-Identifikator als Parameter konstruiert. Das Stylesheet fällt sehr einfach aus, denn es führt lediglich eine Identitätstransformation durch, indem es die unmodifizierten XML-Daten in den Ergebnisbaum kopiert. Das Ergebnis wird dann über dessen File-Konstruktor an ein StreamResult geschickt.

Beispiel: Streams.java

package kapitel5;

import java.io.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;

/**
 * Ein einfaches Beispiel für die JAXP 1.1-Klassen StreamSource und StreamResult.
 * Dieses Programm lädt die XML-Spezifikation vom W3C herunter und schreibt sie
 * in eine temporäre Datei.
 */
public class Streams {
  
    // ein Stylesheet zur Identitätskopie ...
    private static final String IDENTITAETS_XSLT =
        "<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'"
        + " version='1.0'>"
        + "<xsl:template match='/'><xsl:copy-of select='.'/>"
        + "</xsl:template></xsl:stylesheet>";
  
    // ... der XML-Spezifikationen im XML-Format
    // (mittels einer HTTP-URL statt einer file-URL)
    private static String xmlSystemId =
          "http://www.w3.org/TR/2000/REC-xml-20001006.xml";
  
    public static void main(String[] args) throws IOException,
          TransformerException {
            
        // zeigt, wie man von einem System-Identifikator und einem Reader-Objekt liest
       Source xmlSource = new StreamSource(xmlSystemId);
       Source xsltSource = new StreamSource(
               new StringReader(IDENTITAETS_XSLT));
            
       // sendet das Ergebnis an eine Datei
       File ergebnisDatei = File.createTempFile("Streams", ".xml");
       Result ergebnis = new StreamResult(ergebnisDatei);
            
       System.out.println("Die Ergebnisse gehen an: "
               + ergebnisDatei.getAbsolutePath( ));
            
       // die Factory-Instanz erzeugen
       TransformerFactory transFact = TransformerFactory.newInstance( );
            
       // einen Transformer für dieses spezielle Stylesheet erzeugen
       Transformer trans = transFact.newTransformer(xsltSource);
            
       // und die Transformation durchführen
       trans.transform(xmlSource, ergebnis);
   }
}

Das »Identitätskopie«-Stylesheet sucht nur nach /, was ja dem Dokument selbst enspricht. Dann wählt es mit <xsl:copy-of select='.'/> das Dokument aus und kopiert es in den Ergebnisbaum. In diesem Fall haben wir unser eigenes Stylesheet in den Code eingefügt. Man kann das XSLT-Stylesheet aber auch wie folgt auslassen:

// ein Transformer ohne XSLT-Stylesheet wird konstruiert
Transformer trans = transFact.newTransformer( );

Hier liefert der Prozessor sein eigenes Stylesheet und macht dasselbe wie unser Beispiel. Das ist nützlich, wenn Sie zu Debugging-Zwecken einen DOM-Baum mit JAXP in XML-Text konvertieren müssen, weil der Standard-Transformer die XML-Daten ohne jede Transformation einfach kopiert.

JAXP DOM I/O

In vielen Fällen ist die schnellste Form der Transformation, eine Instanz von org.w3c.dom.Document direkt an JAXP zu übergeben. Obwohl die Transformation schnell ist, braucht es Zeit, das DOM zu erzeugen; DOM ist sehr speicherintensiv und eventuell nicht die erste Wahl in großen Dokumenten. In den meisten Fällen werden die DOM-Daten als Ergebnis einer Datenbankabfrage oder eine anderen Arbeitsschritts dynamisch erzeugt (siehe Einführung). Ist das DOM einmal erzeugt, wird das Document-Objekt einfach von einem DOMSource-Objekt wie folgt gekapselt:

org.w3c.dom.Document domDokument = erzeugeDomDokument( );
Source xmlSource = new javax.xml.transform.dom.DOMSource(domDokument);

Der Rest der Transformation sieht exakt so aus, wie die dateibasierte Transformation aus dem Beispiel Streams.java. JAXP braucht nur das alternative Source-Objekt, um die DOM-Daten zu lesen.

JAXP SAX I/O

XSLT ist darauf ausgelegt, wohlgeformte XML-Daten in ein anderes Format, typischerweise HTML, umzuwandeln. Aber wäre es nicht schön, wenn man XSLT-Stylesheets auch nutzen könnte, um Nicht-XML-Daten nach HTML umzuwandeln? Die meisten Tabellenkalkulationen haben beispielsweise die Möglichkeit, ihre Daten im CSV (Comma Separated Values)-Format, wie hier gezeigt, zu exportieren:

Burke,Eric,M
Burke,Jennifer,L
Burke,Aidan,G

Ein Ansatz wäre, die Datei in den Speicher zu parsen, wobei man mit DOM eine XML-Darstellung der Daten erzeugt und die Informationen dann zwecks Transformation an JAXP weiterleitet. Dieser Ansatz funktioniert auch, benötigt aber einen Zwischenschritt in der Programmierung, um die CSV-Datei in einen DOM-Baum umzuwandeln. Eine bessere Möglichkeit ist es, einen eigenen SAX-Parser zu schreiben und dessen Ausgabe an JAXP zu übergeben. Das vermeidet die zusätzliche Erzeugung des DOM-Baums und bietet so bessere Speicherausnutzung und Performance.

Der Ansatz

Es stellt sich schnell heraus, daß es sehr leicht ist, einen SAX-Parser zu schreiben. (Unsere Beispiele verwenden SAX 2.) Alles, was ein SAX-Parser macht, ist eine XML-Datei von oben nach unten zu lesen und Ereignisbenachrichtigungen abzusetzen, wenn er auf die verschiedenen Elemente stößt. In unserem eigenen Parser werden wir die CSV-Datei von oben nach unten lesen und währenddessen SAX-Events absetzen. Ein Programm, das auf diese SAX-Events hört, wird nicht merken, daß die Datendatei im CSV-Format vorliegt und nicht als XML; es bekommt nur die Events mit. Die folgende Abbildung zeigt das konzeptionelle Modell.

Benutzerdefinierter SAX-Parser

Abbildung: Benutzerdefinierter SAX-Parser

In diesem Modell interpretiert der XSLT-Prozessor die SAX-Events als XML-Daten und verwendet ein normales Stylesheet, um die Transformation durchzuführen. Der interessante Aspekt dieses Modells ist, daß wir ganz einfach eigene SAX-Parser für andere Dateiformate schreiben können, was XSLT zu einer nützlichen Transformationssprache für nahezu jede Art vorhandener Anwendungsdaten macht.

In SAX gibt es ein Standard-Interface namens org.xml.sax.XMLReader, das von Parsern implementiert werden muß. Das geschieht in Verbindung mit org.xml.sax.ContentHandler, einem Listener-Interface für SAX-Events. Damit dieses Modell funktioniert, muß Ihr XSLT-Prozessor das ContentHandler-Interface implementieren, damit er auf die SAX-Events hören kann, die XMLReader erzeugt. Im Falle von JAXP wird javax.xml.transform.sax.TransformerHandler dafür verwendet.

Um an eine Instanz von TransformerHandler zu gelangen, sind ein paar zusätzliche Programmierschritte nötig. Erzeugen Sie dazu zuerst wie gehabt ein TransformerFactory-Objekt:

TransformerFactory transFact = TransformerFactory.newInstance( );

Wie zuvor ist TransformerFactory die JAXP-Abstraktion von einem zugrundeliegenden XSLT-Prozessor. Dieser Prozessor muß die SAX-Funktionalitäten ja nicht zwingend unterstützen, weswegen Sie diese Funktionalität zuerst abfragen müssen, um zu ermitteln, ob Sie fortfahren können:

if (transFact.getFeature(SAXTransformerFactory.FEATURE)) {

Wird false zurückgeliefert, haben Sie Pech. Andernfalls können Sie problemlos eine Typumwandlung in SAXTransformerFactory vornehmen und die Instanz von TransformerHandler erzeugen:

SAXTransformerFactory saxTransFact =
         (SAXTransformerFactory) transFact;
// ein ContentHandler ohne Angabe eines Stylesheets wird erzeugt.
// Ohne Stylesheet wird rohes XML ausgegeben.
TransformerHandler transHand = saxTransFact.newTransformerHandler( );

Im hier gezeigten Code wurde kein Stylesheet angegeben. JAXP verwendet standardmäßig das Identitätstransformations-Stylesheet, die SAX-Events werden also in eine rohe XML-Ausgabe »transformiert«. Um ein Stylesheet für die eigentliche Transformation anzugeben, übergeben Sie der Methode wie folgt ein Source-Objekt:

Source xsltSource = new StreamSource(myXsltSystemId);
TransformerHandler transHand = saxTransFact.newTransformerHandler(xsltSource);

Detailliertes Design der CSV nach SAX-Umwandlung

Bevor wir uns an das komplette Beispielprogramm heranmachen, lassen Sie uns einen Schritt zurückmachen und ein detaillierteres Design-Diagramm betrachten. Das konzeptionelle Modell ist klar, aber nun kommen einige Klassen und Interfaces ins Spiel. Die folgende Abbildung zeigt die für SAX-basierte Transformationen notwendigen Bausteine.

SAX und XSLT-Transformationen

Abbildung: SAX und XSLT-Transformationen

Dieses Diagramm erscheint in der Tat komplexer als die vorigen Ansätze, ist aber auf viele Arten ähnlich. In früheren Ansätzen haben wir TransformerFactory verwendet, um Instanzen von Transformer zu erzeugen; im SAX-Ansatz beginnen wir mit einer Unterklasse von TransformerFactory. Bevor mit der Arbeit begonnen werden kann, müssen Sie zunächst sicherstellen, daß Ihre spezielle Implementierung SAX-basierte Transformationen unterstützt. Die Referenzimplementierung von JAXP unterstützt sie, obwohl andere Implementierungen das nicht zwingend müssen. Im folgenden Codefragment liefert die getFeature-Methode von TransformerFactory den Wert true zurück, wenn eine Typumwandlung in SAXTransformerFactory durchgeführt werden kann:

TransformerFactory transFact = TransformerFactory.newInstance( );
if (transFact.getFeature(SAXTransformerFactory.FEATURE)) {
    // Typumwandlung ist erlaubt
    SAXTransformerFactory saxTransFact = (SAXTransformerFactory) transFact;

Wenn getFeature aber false zurückliefert, besteht die einzige Möglichkeit darin, nach einer Implementierung zu suchen, die SAX-basierte Transformationen unterstützt. Ansonsten können Sie mit der Erzeugung einer Instanz von TransformerHandler fortfahren:

TransformerHandler transHand = saxTransFact.newTransformerHandler(myXsltSource);

Dieses Objekt repräsentiert nun Ihr XSLT-Stylesheet. Wie die Abbildung SAX und XSLT-Transformationen zeigt, erweitert TransformerHandler das Interface org.xml.sax.ContentHandler und weiß daher, wie man auf Events eines SAX-Parsers hören muß. Da eine Reihe von SAX-Events die »Schein-XML«-Daten liefern wird, ist das einzige noch fehlende Puzzlestück das Setzen des Result-Objekts und die Anweisung an den SAX-Parser, mit dem Parsen zu beginnen. TransformerHandler bietet auch eine Referenz auf ein Transformer-Objekt. Das ermöglicht Ihnen, Ausgabe-Eigenschaften, wie die Zeichenkodierung, die Einrückung der Ausgabe und andere Attribute von <xsl:output>, festzulegen.

Den eigenen Parser schreiben

Den eigentlichen SAX-Parser zu schreiben klingt schwieriger als es ist. Die Vorgehensweise besteht im wesentlichen in der Implementierung des org.xml.sax.XMLReader-Interface, das zwar zahlreiche Methoden bietet, die man aber in den meisten Anwendungsfällen getrost ignorieren kann. Beim Parsen einer CSV-Datei ist es beispielsweise nicht wirklich notwendig, sich um Namespaces oder Validierung zu kümmern. Der Code für AbstrakterXMLReader.java ist im folgenden Beispiel zu sehen. Es handelt sich um eine abstrakte Klasse, die grundlegende Implementierungen jeder Methode im Interface XMLReader bietet, mit Ausnahme der parse( )-Methode. Das bedeutet, alles, was Sie tun müssen, um einen Parser zu schreiben, ist eine Unterklasse zu erzeugen, die diese einzige Methode überschreibt.

Beispiel: AbstrakterXMLReader.java

package com.oreilly.javaxslt.util;

import java.io.IOException;
import java.util.*;
import org.xml.sax.*;

/**
  * Eine abstrakte Klasse, die das SAX2-XMLReader-Interface imlementiert. Die
  * Absicht hinter dieser Klasse ist die Erleichterung für Unterklassen, als
  * SAX2-XMLReader-Implementierungen zu agieren. Das ermöglicht Ihnen beispielsweise,
  * SAX2-Events abzusetzen, die zwecks Transformation an einen XSLT-Prozessor
  * übergeben werden können.
  */
public abstract class AbstrakterXMLReader implements org.xml.sax.XMLReader {
    private Map featureMap = new HashMap( );
    private Map propertyMap = new HashMap( );
    private EntityResolver entityResolver;
    private DTDHandler dtdHandler;
    private ContentHandler contentHandler;
    private ErrorHandler errorHandler;
  
    /**
     * Die einzige abstrakte Methode in dieser Klasse. Abgeleitete Klassen können
     * jede Datenquelle parsen und SAX2-Events an ContentHandler senden.
     */
    public abstract void parse(InputSource input) throws IOException,
        SAXException;
  
    public boolean getFeature(String name)
        throws SAXNotRecognizedException, SAXNotSupportedException {
      Boolean featureWert = (Boolean) this.featureMap.get(name);
      return (featureWert == null) ? false
            : featureWert.booleanValue( );
    }
  
    public void setFeature(String name, boolean value)
        throws SAXNotRecognizedException, SAXNotSupportedException {
     this.featureMap.put(name, new Boolean(value));
    }
  
    public Object getProperty(String name)
        throws SAXNotRecognizedException, SAXNotSupportedException {
      return this.propertyMap.get(name);
    }
  
    public void setProperty(String name, Object value)
        throws SAXNotRecognizedException, SAXNotSupportedException {
      this.propertyMap.put(name, value);
    }
  
    public void setEntityResolver(EntityResolver entityResolver) {
      this.entityResolver = entityResolver;
    }
  
    public EntityResolver getEntityResolver( ) {
      return this.entityResolver;
    }
  
    public void setDTDHandler(DTDHandler dtdHandler) {
      this.dtdHandler = dtdHandler;
    }
  
    public DTDHandler getDTDHandler( ) {
      return this.dtdHandler;
    }
  
    public void setContentHandler(ContentHandler contentHandler) {
      this.contentHandler = contentHandler;
    }
  
    public ContentHandler getContentHandler( ) {
      return this.contentHandler;
    }
  
    public void setErrorHandler(ErrorHandler errorHandler) {
      this.errorHandler = errorHandler;
    }
  
    public ErrorHandler getErrorHandler( ) {
      return this.errorHandler;
    }
  
    public void parse(String systemId) throws IOException, SAXException {
      parse(new InputSource(systemId));
    }
}

Die Erzeugung der Unterklasse CSVXMLReader beinhaltet das Überschreiben der parse( )-Methode und das eigentliche Durchgehen der CSV-Datei, wobei SAX-Events abgesetzt werden, wenn Elemente in der Datei gefunden werden. Während der SAX-Teil sehr einfach ist, stellt das Parsen der CSV-Datei doch eine größere Herausforderung dar. Um diese Klasse so flexibel wie möglich zu gestalten, wurde sie darauf ausgelegt, jede CSV-Datei zu parsen, die eine Tabellenkalkulation, wie Microsoft Excel, exportieren kann. Bei einfachen Daten könnte Ihre CSV-Datei also wie folgt aussehen:

Burke,Eric,M
Burke,Jennifer,L
Burke,Aidan,G

Die XML-Darstellung dieser Datei sehen Sie im folgenden Beispiel. Der einzig wirkliche Nachteil hier ist, daß CSV-Dateien streng positionsorientiert sind, die Spalten also keine Namen tragen. Daher enthält die XML-Ausgabe lediglich eine Reihe von drei <wert>-Elementen für jede Zeile, und Ihr Stylesheet muß die Bestandteile nach deren Position auswählen.

Beispiel: Beispielhafte XML-Ausgabe eines CSV-Parsers

<?xml version="1.0" encoding="ISO-8859-1"?>
<csvDatei>
    <zeile>
        <wert>Burke</wert>
        <wert>Eric</wert>
        <wert>M</wert>
    </zeile>
    <zeile>
        <wert>Burke</wert>
        <wert>Jennifer</wert>
        <wert>L</wert>
    </zeile>
    <zeile>
        <wert>Burke</wert>
        <wert>Aidan</wert>
        <wert>G</wert>
    </zeile>
</csvDatei>

Eine Verbesserung wäre eine veränderte Auslegung des CSV-Parsers, so daß dieser eine Liste von aussagekräftigen Spaltennamen als Parameter akzeptieren könnte, und diese könnten dann im erzeugten XML verwendet werden. Eine weitere Option wäre es, ein XSLT-Stylesheet zu schreiben, das diese anfängliche Ausgabe in ein anderes XML-Format mit aussagekräftgen Spaltenüberschriften transformieren würde. Damit das Codebeispiel relativ leicht zu handhaben bleibt, werden diese Funktionalitäten in dieser Implementierung weggelassen. Es gibt jedoch einige Eigenheiten des CSV-Dateiformats, die bedacht werden müssen. So müssen beispielsweise Felder, die Kommas enthalten, in Anführungszeichen eingeschlossen werden:

"Berater,Autor,Lehrer",Burke,Eric,M
Lehrerin,Burke,Jennifer,L
kein,Burke,Aidan,G

Um die Sache noch komplizierter zu machen, können Felder auch Anführungszeichen (") enthalten. In diesem Fall werden die Anführungszeichen verdoppelt, so ähnlich, wie man in Java zwei Backslash-Zeichen (\\) verwendet, um einen einzelnen Backslash darzustellen. Im folgenden Beispiel enthält die erste Spalte ein einzelnes Anführungszeichen, weswegen das ganze Feld in Anführungszeichen gesetzt und das einzelne Anführungszeichen verdoppelt wird:

"test""anführungszeichen",Lehrerin,Burke,Jennifer,L

Das würde interpretiert als:

test"anführungszeichen,Lehrerin,Burke,Jennifer,L

Der Code im folgenden Beispiel zeigt die komlette Implementierung des CSV-Parsers.

Beispiel: CSVXMLReader.java

package com.oreilly.javaxslt.util;

import java.io.*;
import java.net.URL;

import org.xml.sax.*;
import org.xml.sax.helpers.*;
/**
 * Eine Tool-Klasse, die eine Datei mit kommaseparierten Werten (CSV-Datei) parst
 * und deren Inhalte über SAX2-Events ausgibt. Das CSV-Format, das diese Klasse
 * lesen kann, ist identisch zum Exportformat von Microsoft Excel.
 * Bei einfachen Werten könnte die CSV-Datei wie folgt aussehen:
 * <pre>
 * a,b,c
 * d,e,f
 * </pre>
 * Anführungszeichen schließen den Wert ein, wenn er Kommas enthält:
 * <pre>
 * a,"b,c",d
 * e,"f,g","h,i"
 * </pre>
 * Wenn der Wert Anführungszeichen enthält, werden diese verdoppelt. Der Parser
 * entfernt auch an ein Komma angrenzende Leerzeichen.
 *
 * @author Eric M. Burke
 */
public class CSVXMLReader extends AbstrakterXMLReader {
  
  // ein leeres Attribut zur Verwendung mit SAX
  private static final Attributes LEERES_ATTR = new AttributesImpl( );
  
  /**
   * Eine CSV-Datei wird geparst. SAX-Events werden an das ContentHandler-Objekt,
   * das über <code>setContentHandler</code> registriert wurde, übergeben.
   *
   * @param input enthält die zu parsende CSV-Datei.
   */
  public void parse(InputSource input) throws IOException,
      SAXException {
  // wenn kein Handler zum Empfang von Events registriert wurde,
  // braucht die CSV-Datei auch nicht geparst zu werden.
  ContentHandler ch = getContentHandler( );
  if (ch == null) {
      return;
  }
  
  // Das InputSource-Objekt wird in einen BufferedReader konvertiert
  BufferedReader br = null;
  if (input.getCharacterStream( ) != null) {
      br = new BufferedReader(input.getCharacterStream( ));
  } else if (input.getByteStream( ) != null) {
      br = new BufferedReader(new InputStreamReader(
           input.getByteStream( )));
  } else if (input.getSystemId( ) != null) {
      java.net.URL url = new URL(input.getSystemId( ));
      br = new BufferedReader(new InputStreamReader(url.openStream( )));
  } else {
      throw new SAXException("Ungültiges InputSource-Objekt");
  }
  
  ch.startDocument( );
  
  // sende <csvDatei>
  ch.startElement("","","csvDatei",LEERES_ATTR);
  
  // nun wird jede Zeile der Datei gelesen, bis EOF erreicht ist.
  String aktZeile = null;
  while ((aktZeile = br.readLine( )) != null) {
     aktZeile = aktZeile.trim( );
     if (aktZeile.length( ) > 0) {
        // erzeuge das <zeile>-Element
        ch.startElement("","","zeile",LEERES_ATTR);
        // und gib die Daten dieser Zeile aus.
        parseZeile(aktZeile, ch);
        // schließe das </zeile>-Element
        ch.endElement("","","zeile");
    }
  }

  // sende </csvDatei>
  ch.endElement("","","csvDatei");
  ch.endDocument( );
}

//Eine einzelne Zeile wird in Token aufgebrochen. Diese rekursive Funktion
// extrahiert das erste Token und parst dann rekursiv den Rest der Zeile.
private void parseZeile(String aktZeile, ContentHandler ch)
  throws IOException, SAXException {
    
  String erstesToken = null;
  String restDerZeile = null;
  int kommaIndex = findeErstesTrennzeichen(aktZeile);
  if (kommaIndex > -1) {
     erstesToken = aktZeile.substring(0, kommaIndex).trim( );
     restDerZeile = aktZeile.substring(kommaIndex+1).trim( );
  } else {
     // keine Kommas, also ist die ganze Zeile ein Token
     erstesToken = aktZeile;
  }

  // entferne überflüssige Anführungszeichen
  erstesToken = bereinigeAnfuehrungszeichen(erstesToken);

  // sende das <wert>-Element
  ch.startElement("","","wert",LEERES_ATTR);
  ch.characters(erstesToken.toCharArray(), 0, erstesToken.length( ));
  ch.endElement("","","wert");

  // den Rest der Zeile rekursiv verarbeiten
  if (restDerZeile != null) {
     parseZeile(restDerZeile, ch);
  }
}

// finde die Position des Kommas und berücksichtige, daß Kommas in Token, die in
// Anführungszeichen eingeschlossen sind, ignoriert werden können.
private int findeErstesTrennzeichen(String aktZeile) {
  if (aktZeile.startsWith("\"")) {
     boolean inAnfuehrungszeichen = true;
     int zeichenZahl = aktZeile.length( );
     for (int i=1; i<zeichenZahl; i++) {
       char aktZeichen = aktZeile.charAt(i);
       if (aktZeichen == '"') {
          inAnfuehrungszeichen = !inAnfuehrungszeichen;
       } else if (aktZeichen == ',' && !inAnfuehrungszeichen) {
          return i;
       }
     }
     return -1;
  } else {
     return aktZeile.indexOf(',');
  }
}

// entfernt Anfuehrungszeichen um ein Token und doppelte Anfuehrungszeichen
// innerhalb eines Tokens.
private String bereinigeAnfuehrungszeichen(String token) {
  StringBuffer buf = new StringBuffer( );
  int laenge = token.length( );
  int aktIndex = 0;
  
  if (token.startsWith("\"") && token.endsWith("\"")) {
     aktIndex = 1;
     laenge--;
  }
  
  boolean einAnfuehrungszeichenGefunden = false;
  boolean zweiAnfuehrungszeichenGefunden = false;
  
  while (aktIndex < laenge) {
  char aktZeichen = token.charAt(aktIndex);
  if (aktZeichen == '"') {
      zweiAnfuehrungszeichenGefunden = (einAnfuehrungszeichenGefunden) ?
      true : false;
      einAnfuehrungszeichenGefunden = true;
  } else {
      einAnfuehrungszeichenGefunden = false;
      zweiAnfuehrungszeichenGefunden = false;
  }

  if (zweiAnfuehrungszeichenGefunden) {
      zweiAnfuehrungszeichenGefunden = false;
      einAnfuehrungszeichenGefunden = false;
      aktIndex++;
      continue;
  }

  buf.append(aktZeichen);
  aktIndex++;
  }

     return buf.toString( );
  }
}
  

Da CSVXMLReader eine Unterklasse von AbstrakterXMLReader ist, muß CSVXMLReader die abstrakte parse-Methode implementieren:

public void parse(InputSource input) throws IOException,
      SAXException {
   // wenn kein Handler zum Empfang von Events registriert wurde,
   // braucht die CSV-Datei auch nicht geparst zu werden.
   ContentHandler ch = getContentHandler();
   if (ch == null) {
      return;
   }

Das erste, was diese Methode macht, ist die Existenz eines SAX-ContentHandler zu überprüfen. Die Basisklasse AbstractXMLReader bietet einen Zugriff auf dieses Objekt, das auf die SAX-Events hören muß. In unserem Beispiel wird eine Instanz von JAXPs TransformerHandler als Implementierung für SAXs ContentHandler verwendet. Wenn dieser Handler nicht registriert ist, kehrt unsere parse-Methode zurück, weil es keinen Event-Listener gibt. In einem echten SAX-Parser würden die XML-Daten trotzdem geparst, was die Möglichkeit bietet, die XML-Daten auf Fehler zu prüfen. Die Entscheidung, unmittelbar aus der Methode zurückzukehren, ist hier lediglich eine Performance-Optimierung für diese Klasse.

Der SAX-InputSource-Parameter erlaubt unserem Parser, die CSV-Datei zu lokalisieren. Da ein InputSource-Objekt viele Möglichkeiten hat, seine Daten einzulesen, müssen Parser jede potentielle Quelle in der hier gezeigten Reihenfolge überprüfen:

// Das InputSource-Objekt wird in einen BufferedReader konvertiert
BufferedReader br = null;
if (input.getCharacterStream() != null) {
  br = new BufferedReader(input.getCharacterStream());
} else if (input.getByteStream() != null) {
  br = new BufferedReader(new InputStreamReader(
  input.getByteStream()));
} else if (input.getSystemId() != null) {
  java.net.URL url = new URL(input.getSystemId());
  br = new BufferedReader(new InputStreamReader(url.openStream()));
} else {
  throw new SAXException("Ungültiges InputSource-Objekt");
}

Unter der Annahme, daß unser InputSource-Objekt gültig war, können wir nun mit dem Parsen der CSV-Datei und dem Absetzen von SAX-Events beginnen. Der erste Schritt ist die Benachrichtigung des ContentHandler, daß ein neues Dokument angefangen hat:

ch.startDocument( );

// sende <csvDatei>
ch.startElement("","","csvDatei",LEERE_ATTR);

Der XSLT-Prozessor interpretiert dies wie folgt:

<?xml version="1.0" encoding="UTF-8"?>
<csvDatei>

Unser Parser ignoriert viele SAX 2-Funktionen einfach, vor allem XML-Namespaces. Daher enthalten so viele Werte, die als Parameter an die zahlreichen ContentHandler-Methoden übergeben werden, leere Strings. Die Konstante LEERES_ATTR zeigt an, daß dieses XML-Element keine Attribute hat.

Die CSV-Datei selbst ist recht eindeutig, daher iterieren wir lediglich über jede Zeile in der Datei und senden SAX-Events, wenn wir eine Zeile lesen. Die parseZeile-Methode ist eine private Hilfsmethode, die das eigentliche CSV-Parsing durchführt:

// nun wird jede Zeile der Datei gelesen, bis EOF erreicht ist.
String aktZeile = null;
while ((aktZeile = br.readLine()) != null) {
  aktZeile = aktZeile.trim();
  if (aktZeile.length() > 0) {
    // erzeuge das <zeile>-Element
    ch.startElement("","","zeile",LEERES_ATTR);
    // und gib die Daten dieser Zeile aus.
    parseZeile(aktZeile, ch);
    // schließe das </zeile>-Element
    ch.endElement("","","zeile");
  } 
}

Und schließlich müssen wir noch anzeigen, daß das Parsing vollständig ist:

// sende </csvDatei>
ch.endElement("","","csvDatei");
ch.endDocument( );

Die übrigen Methoden in CSVXMLReader werden hier nicht detailliert besprochen, weil sie nur dafür zuständig sind, jede Zeile in der CSV-Datei aufzubrechen und niedere Parsing-Aufgaben zu erledigen, wie nach Kommas und Anführungszeichen zu suchen. Bemerkenswert ist jedoch der Code, der Text wie den Folgenden aussendet:

<wert>hier steht Text</wert>

SAX-Parser nutzen die characters-Methode von ContentHandler, um Text darzustellen. Diese Methode hat die folgende Signatur:

public void characters(char[] ch, int start, int length)

Obwohl diese Methode auch einen String hätte aufnehmen können, erlaubt die Verwendung eines Arrays den SAX-Parsern, ein großes Zeichen-Array im voraus zu alloziieren und diesen Puffer dann wiederholt zu verwenden. Daher kann eine Implementierung von ContentHandler nicht einfach annehmen, daß das komplette ch-Array sinnvolle Daten enthält. Statt dessen darf es nur die angegebene Zeichenanzahl, angefangen an der start-Position, einlesen.

Unser Parser verwendet einen recht naheliegenden Ansatz, indem er einen String in ein Zeichen-Array umwandelt und dieses dann als Parameter an die characters-Methode übergibt:

// sende das <wert>-Element
ch.startElement("","","wert",LEERES_ATTR);
ch.characters(erstesToken.toCharArray(), 0, erstesToken.length());
ch.endElement("","","wert");

Den Parser verwenden

Lassen Sie uns zum Abschluß betrachten, wie man unseren CSV-Parser nun mit einem XSLT-Stylesheet einsetzt. Der Code im folgenden Beispiel ist eine eigenständige Java-Anwendung, mit der Sie XSLT-Transformationen auf CSV-Dateien anwenden können. Wie die Kommentare andeuten, erwartet sie den Namen einer CSV-Datei als ersten Parameter und als optionalen, zweiten Parameter den Namen eines XSLT-Stylesheets. Die komplette Ausgabe wird an System.out geschickt.

Beispiel: SimplerCSVProzessor.java

package com.oreilly.javaxslt.util;

import java.io.*;
import javax.xml.transform.*;
import javax.xml.transform.sax.*;
import javax.xml.transform.stream.*;
import org.xml.sax.*;

/**
 * Zeigt, wie man die CSVXMLReader-Klasse einsetzt. Es handelt sich um ein
 * Kommandozeilen-Tool, das eine CSV-Datei und optional eine XSLT-Datei als
 * Kommandozeilenparameter erwartet. Die Transformation wird dann durchgeführt
 * und die Ausgabe an System.out geschickt.
 */
public class SimplerCSVProzessor {
  
  public static void main(String[] args) throws Exception {
    if (args.length == 0) {
      System.err.println("Syntax: java "
          + SimplerCSVProzessor.class.getName( )
          + " <csvDatei> [xsltDatei]");
      System.err.println(" - csvDatei ist erforderlich");
      System.err.println(" - xsltDatei ist optional");
      System.exit(1);
    }
  
    String csvDateiname = args[0];
    String xsltDateiname = (args.length > 1) ? args[1] : null;
  
    TransformerFactory transFact = TransformerFactory.newInstance( );
    if (transFact.getFeature(SAXTransformerFactory.FEATURE)) {
      SAXTransformerFactory saxTransFact =
         (SAXTransformerFactory) transFact;
      TransformerHandler transHand = null;
      if (xsltDateiname == null) {
         transHand = saxTransFact.newTransformerHandler( );
      } else {
         transHand = saxTransFact.newTransformerHandler(
           new StreamSource(new File(xsltDateiname)));
      }
  
    // das Ziel der XSLT-Transformation wird gesetzt
    transHand.setResult(new StreamResult(System.out));
  
    // der CSVXMLReader wird an die CSV-Datei gebunden
    CSVXMLReader csvReader = new CSVXMLReader( );
    InputSource csvInputSrc = new InputSource(
       new FileReader(csvDateiname));
  
    // der XSLT-Prozessor wird mit dem CSVXMLReader verbunden
    csvReader.setContentHandler(transHand);
    csvReader.parse(csvInputSrc);
  } else {
    System.err.println("SAXTransformerFactory wird nicht unterstützt.");
    System.exit(1);
  }
 }
}

Wie in diesem Kapitel schon erwähnt, wird TransformerHandler von JAXP bereitgestellt und ist eine Implementierung des org.xml.sax.ContentHandler-Interface. Die Konstruktion erfolgt, wie hier gezeigt, über SAXTransformerFactory:

TransformerHandler transHand = null;
if (xsltDateiname == null) {
    transHand = saxTransFact.newTransformerHandler( );
} else {
   transHand = saxTransFact.newTransformerHandler(
     new StreamSource(new File(xsltDateiname)));
}

Wird kein XSLT-Stylesheet angegeben, erfolgt eine Identitätstransformation. Das ist nützlich, wenn Sie lediglich die XML-Rohdaten ohne Anwendung eines Stylesheets ansehen möchten. Sie werden wahrscheinlich auch zunächst so vorgehen, um zu planen, wie Sie Ihr XSLT-Stylesheet schreiben müssen. Wird ein Stylesheet angegeben, wird es auf jeden Fall zur Transformation verwendet.

Danach wird dann der benutzerdefinierte Parser wie folgt konstruiert:

CSVXMLReader csvReader = new CSVXMLReader( );

Der Speicherort der CSV-Datei wird danach in ein SAX InputSource-Objekt konvertiert:

InputSource csvInputSrc = new InputSource(
        new FileReader(csvDateiname));

Und schließlich wird der XSLT-Prozessor mit unserem Parser verbunden. Das wird über die Registrierung von TransformerHandler als ContentHandler bei csvReader erreicht. Ein einzelner Aufruf der parse-Methode startet dann das Parsing und die Transformation:

// der XSLT-Prozessor wird mit dem CSVXMLReader verbunden
csvReader.setContentHandler(transHand);
csvReader.parse(csvInputSrc);

Lassen Sie uns für einen einfachen Test annehmen, die altbekannte Liste der Bundespräsidenten läge im CSV-Format vor:

Heuss,Theodor,
Lübke,Heinrich,
Heinemann,Gustav,W,
Scheel,Walter,
  etc...
Rau,Johannes

Um zu sehen, wie das resultierende XML aussieht, rufen Sie das Programm wie folgt auf:

java com.oreilly.javaxslt.util.SimplerCSVProzessor bundespraesidenten.csv

Nun wird die CSV-Datei geparst und das Identitätstransformations-Stylesheet angewandt, was die folgende Ausgabe auf die Konsole bringt:

<?xml version="1.0" encoding="UTF-8"?>
<csvDatei>
    <zeile>
        <wert>Heuss</wert>
        <wert>Theodor</wert>
        <wert/>
        <wert/>
    </zeile>
    <zeile>
     etc...
</csvDatei>

Tatsächlich wird die Ausgabe in eine einzelne, lange Zeile gestopft. Hier ist sie zur besseren Lesbarkeit aufgebrochen. Jede gute XML-Editor-Anwendung sollte über eine Funktion zur besser lesbaren Darstellung von XML-Daten verfügen. Um die Daten nun in etwas Brauchbares zu transformieren, muß ein Stylesheet her. Das im folgenden Beispiel gezeigte Stylesheet wandelt die Ausgabe dieses Programms in eine HTML-Tabelle um.

Beispiel: csvZuHTMLTabelle.xslt

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="html"/>
    <xsl:template match="/">
        <table border="1">
            <xsl:apply-templates select="csvDatei/zeile"/>
        </table>
    </xsl:template>
    <xsl:template match="zeile">
        <tr>
            <xsl:apply-templates select="wert"/>
        </tr>
    </xsl:template>
    <xsl:template match="wert">
        <td>
            <!-- Wenn ein Wert leer ist, wird ein umbruchgeschütztes Leerzeichen ausgegeben, damit die HTML-Tabelle korrekt aufgebaut werden kann-->
            <xsl:if test=".=''">
                <xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text>
            </xsl:if>
            <xsl:value-of select="."/>
        </td>
    </xsl:template>
</xsl:stylesheet>

Um das Stylesheet nun anzuwenden, geben Sie das folgende Kommando ein:

java com.oreilly.javaxslt.util.SimplerCSVProzessor bundespraesidenten.csv csvZuHTMLTabelle.xslt

Wie zuvor werden die Ergebnisse, diesmal also der Code für eine HTML-Tabelle, auf System.out ausgegeben. Das Stylesheet wird auf jede von SimplerCSVProzessor geparsten CSV-Datei anwendbar sein und ist nicht etwa auf bundespraesidenten.xml beschränkt. Nun, da das Konzept bestätigt wurde, können Sie der HTML-Ausgabe Formatierungen und eigene Ausgaben hinzufügen, ohne Java-Code zu ändern – bearbeiten Sie einfach das Stylesheet oder schreiben Sie ein eigenes.

Schlußfolgerung

Obwohl das Schreiben eines SAX-Parsers, den man mit JAXP verbindet, einige in wechselseitiger Beziehung zueinander stehende Klassen beinhaltet, kommt die daraus hervorgehende Anwendung doch mit nur zwei Kommandozeilenparametern aus und funktioniert mit jeder CSV- oder XSLT-Datei. Was dieses Beispiel so interessant macht, ist die Tatsache, daß derselbe Ansatz im Grunde genommen mit jeder Datenquelle funktioniert. Die Schritte werden wie folgt heruntergebrochen:

  1. Erstellen Sie einen eigenen SAX-Parser durch Implementierung von org.xml.sax.XMLReader oder Erweiterung von com.oreilly.javaxslt.util.AbstrakterXMLReader.
  2. Senden Sie aus Ihrem Parser heraus die passenden SAX-Events, während Sie die Daten lesen.
  3. Modifizieren Sie SimplerCSVProzessor, um Ihren eigenen Parser statt CSVXMLReader zu nutzen.

Vielleicht möchten Sie ja einmal einen eigenen Parser schreiben, der statt einer CSV-Datei ein SQL-Statement akzeptiert. Ihr Parser könnte sich dann mit einer Datenbank verbinden, die Anfrage stellen und dann für jede Zeile im ResultSet SAX-Events absetzen. Das macht die Datenextraktion aus jeder beliebigen, relationalen Datenbank sehr einfach, weil nicht viel anwendungsspezifischer Code geschrieben werden muß. Es entfernt ebenfalls den Zwischenschritt der JDOM- bzw. DOM-Erzeugung, weil die SAX-Events direkt zur Transformation an JAXP übergeben werden.

JDOM-Ausgaben an JAXP übergeben

Da die Verwendung der DOM-API sehr mühselig ist, setzen viele Java-Programmierer statt dessen lieber auf JDOM. Die typische Vorgehensweise ist dabei die dynamische Erzeugung von XML mittels JDOM, das danach mit XSLT in eine Webseite transformiert wird. Daraus ergibt sich jedoch ein Problem, denn JAXP bietet keine direkte Implementierung des javax.xml.Source-Interface für JDOM. (Während das hier geschrieben wurde, haben Mitglieder der JDOM-Community eine JDOM-Implementierung von javax.xml.Source erstellt, die unmittelbar mit JAXP zusammenarbeiten wird.) Es gibt daher mindestens drei Möglichkeiten:

  • Verwenden Sie org.jdom.output.SAXOutputter, um SAX 2-Events von JDOM an JAXP zu leiten.
  • Verwenden Sie org.jdom.output.DOMOutputter, um den JDOM-Baum in einen DOM-Baum umzuwandeln, und lesen Sie dann mit javax.xml.transform.dom.DOMSource die Daten in JAXP ein.
  • Verwenden Sie org.jdom.output.XMLOutputter, um den JDOM-Baum in XML-Text zu serialisieren, und parsen Sie das XML dann mit java.xml.transform.stream.StreamSource zurück in JAXP.

Möglichkeit 1: JDOM in SAX umwandeln

Der SAX-Ansatz ist generell anderen Ansätzen vorzuziehen. Sein Hauptvorteil ist, daß er keine Zwischentransformation benötigt, um den JDOM-Baum in einen DOM-Baum oder Text umzuwandeln. Dadurch entsteht der geringste Speicherbedarf und die potentiell schnellste Performance.

Zur Unterstützung von SAX bietet JDOM die Klasse org.jdom.output.SAXOutputter. Das folgende Codefragment zeigt deren Einsatz:

TransformerFactory transFact = TransformerFactory.newInstance( );
if (transFact.getFeature(SAXTransformerFactory.FEATURE)) {
  SAXTransformerFactory stf = (SAXTransformerFactory) transFact;
  // der 'stylesheet'-Parameter ist eine Instanz von JAXPs
  // javax.xml.transform.Templates-Interface
  TransformerHandler transHand = stf.newTransformerHandler(stylesheet);
  
  // Ergebnis ist eine Result-Instanz
  transHand.setResult(ergebnis);
  SAXOutputter saxOut = new SAXOutputter(transHand);
  // der 'jdomDoc'-Parameter ist eine Instanz von JDOMs
  // org.jdom.Document-Klasse. Er enthält die XML-Daten.
  saxOut.output(jdomDoc);
} else {
  System.err.println("SAXTransformerFactory wird nicht unterstützt");
}

Möglichkeit 2: JDOM in DOM umwandeln

Der DOM-Ansatz ist grundsätzlich etwas langsamer und wird nicht funktionieren, wenn JDOM eine andere DOM-Implementierung verwendet als JAXP. JDOM kann nämlich, wie JAXP, hinter den Kulissen verschiedene DOM-Implementierungen einsetzen. Wenn JDOM sich auf eine andere DOM-Version als JAXP bezieht, werden Exceptions auftreten, wenn Sie mit der Transformation beginnen. Da JAXP standardmäßig Apaches Crimson-Parser verwendet, können Sie JDOM mit der Klasse org.jdom.adapters.CrimsonDOM Adapter so konfigurieren, daß es auch Crimson verwendet. Der folgende Code zeigt, wie man ein JDOM-Dokument in ein DOM-Dokument umwandelt:

org.jdom.Document jdomDoc = createJDOMDocument( );
// hier werden dem JDOM-Dokument Daten hinzugefügt
...

// dann wird das JDOM-Dokument in ein DOM-Dokument umgewandelt
org.jdom.output.DOMOutputter domOut = new org.jdom.output.DOMOutputter(
  "org.jdom.adapters.CrimsonDOMAdapter");
org.w3c.dom.Document domDoc = domOut.output(jdomDoc);

Zeile 6 ist hervorgehoben, weil diese Ihnen höchstwahrscheinlich die meisten Probleme bereitet. Wenn JDOM seinen internen Objektbaum in einen DOM-Objektbaum umwandelt, muß es eine darunterliegende DOM-Implementierung verwenden. In vielerlei Hinsicht ist JDOM JAXP sehr ähnlich, weil es viele Aufgaben an tieferliegende Klassen delegiert. Die Konstruktoren von DOMOutputter werden wie folgt überladen:

// verwende die Standard-Adapterklasse
public DOMOutputter( )

// verwende die übergebene Adapterklasse
public DOMOutputter(String adapterClass)

Der erste hier gezeigte Konstruktor wird JDOMs Standard-DOM-Parser nutzen, der nicht notwendigerweise derselbe DOM-Parser ist, den JAXP nutzt. Die zweite Methode erlaubt die Angabe einer Adapterklasse, die das Interface org.jdom.adapters.DOMAdapter implementieren muß. JDOM beinhaltet Standard-Adapterklassen für alle verbreiteten DOM-Implementierungen, Sie können aber auch eine eigene Adapterklasse schreiben.

Möglichkeit 3: JDOM zu Text konvertieren

Im letzten Ansatz können Sie java.io.StringWriter und java.io.StringReader nutzen. Erzeugen Sie dazu zuerst wie gewöhnlich die JDOM-Daten, und nutzen Sie dann org.jdom.output.XMLOutputter, um die Daten in einen XML-String zu konvertieren:

StringWriter sw = new StringWriter( );
org.jdom.output.XMLOutputter xmlOut
  = new org.jdom.output.XMLOutputter("", false);
xmlOut.output(jdomDoc, sw);

Die Parameter für XMLOutputter gestatten neben der Angabe der Einrückungstiefe für die Ausgabe die Angabe eines boolean-Flags, das angibt, ob Zeilenumbrüche gemacht werden sollen. Im Codebeispiel werden keine Leerzeichen oder Zeilenumbrüche angegeben, um die Größe des erzeugten XML zu minimieren. Nun, da das StringWriter-Objekt Ihre XML-Daten enthält, können Sie ein StringReader-Objekt zusammen mit javax.xml.transform.stream.StreamSource verwenden, um die Daten in JAXP einzulesen:

StringReader sr = new StringReader(sw.toString( ));
Source xmlSource = new javax.xml.transform.stream.StreamSource(sr);

Die Transformation kann dann, wie bereits im Beispiel Streams.java geschehen, fortgesetzt werden. Der Hauptnachteil dieser Methode liegt darin, daß die XML-Daten erst in Text umgewandelt und danach wieder zurückgeparst werden müssen, bevor die Transformation angewandt werden kann.

   

zum Seitenanfang

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Java & XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2002 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Java und XSLT" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de