Testen und Fehlerbeseitigung

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

   

   

Unter Entwicklern ist in den letzten Jahren ein verstärktes Interesse für das Testen von Software aufgekommen. Dabei war die Programmiermethode eXtreme eine wichtige Triebkraft, die großen Wert auf kleine Einheiten und ständiges Testen legt, um eine hohe Qualität der Software zu garantieren. Weitere Informationen zu eXtreme-Programming finden Sie unter Xprogramming.com. Um das Testen von XSLT-Transformationen zu demonstrieren, werden einige Dateien benötigt. Die XML-Daten sind im folgenden Beispiel zu sehen.

Beispiel: aidan.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<person>
    <vorname>Aidan</vorname>
    <vorname2>Garrett</vorname2>
    <nachname>Burke</nachname>
    <geburtstag tag="25" monat="6" jahr="1999"/>
</person>

Diese Daten sind trivial, das Konzept gilt aber auch für größere und realistischere Beispiele. Das zugehörige XSLT-Stylesheet ist im folgenden Beispiel zu sehen.

Beispiel: condensePerson.xslt

<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
 ***********************************************************
 ** Transformiert eine XML-Datei mit der Repräsentation einer
 ** Person in ein übersichtlicheres Format.
 ********************************************************-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:param name="mitVorname2" select="'ja'"/>
    <xsl:output method="xml" version="1.0" encoding="ISO-8859-1" indent="yes" doctype-system="condense.dtd"/>
    <!-- Template für das Element <person> -->
    <xsl:template match="person">
        <!-- erzeuge ein neues Element <person> in verkürzter Form -->
        <xsl:element name="person">
            <xsl:element name="name">
                <xsl:value-of select="vorname"/>
                <xsl:text> </xsl:text>
                <xsl:if test="$mitVorname2 = 'ja'">
                    <xsl:value-of select="vorname2"/>
                    <xsl:text> </xsl:text>
                </xsl:if>
                <xsl:value-of select="nachname"/>
            </xsl:element>
            <xsl:element name="geburtstag">
                <xsl:value-of select="geburtstag/@tag"/>
                <xsl:text>.</xsl:text>
                <xsl:value-of select="geburtstag/@monat"/>
                <xsl:text>.</xsl:text>
                <xsl:value-of select="geburtstag/@jahr"/>
            </xsl:element>
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

Mit diesem Stylesheet werden die XML-Daten in das prägnante Format aus dem folgenden Beispiel transformiert.

Beispiel: Erwartete Ausgabe

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE person SYSTEM "condense.dtd">
<person>
    <name>Aidan Garrett Burke</name>
    <geburtstag>25.6.1999</geburtstag>
</person>

Das folgende Beispiel zeigt schließlich die DTD für das verkürzte XML-Format.

Beispiel: condense.dtd

<!ELEMENT person (name, geburtstag)>
<!ELEMENT geburtstag (#PCDATA)>
<!ELEMENT name (#PCDATA)>

Die DTD für die XML-Ausgabe ermöglicht es, das Ergebnis einer oder mehrerer Transformationen durch einen Unit-Test zu validieren. Ein solcher Test schreibt das Ergebnis einer Transformation in eine Datei und versucht diese anschließend mit einem XML-Parser einzulesen und somit zu validieren.

JUnit

JUnit ist ein Open Source Test-Framework. Es handelt sich um ein handliches Werkzeug, das speziell für Unit-Tests entwickelt wurde. Andere Werkzeuge sind oft besser für Integrationstests und funktionale Tests geeignet, diese werden hier aber nicht besprochen.

Da XSLT-Transformationen unabhängig vom Rest einer Anwendung durchgeführt werden können, sind sie für automatisierte Unit-Tests perfekt geeignet. Eine Technologie wie JSP kann dagegen nicht außerhalb eines JSP-Containers und Webbrowsers ausgeführt werden und kann somit auch nur schwer automatisch getestet werden.

Ein automatisierter Test liefert nach seiner Ausführung eine Meldung über »Erfolg« bzw. »Mißerfolg«, ohne daß es während des Tests einer Interaktion mit einem Bediener bedarf. Zum Beispiel kann ein Test, bei dem ein Nutzer bestimmte Werte in die Felder eines HTML-Formulars eintragen muß, um dann die resultierende Webseite zu begutachten, keinesfalls als automatisiert bezeichnet werden. Ein Test, der im Ergebnis einen längeren Bericht liefert, ist ebenfalls nicht automatisiert, da es einer sachkundigen Person bedarf, die diesen Text liest und Fehler ermittelt.

Dagegen kann ein Entwickler eine Menge von automatisierten Tests mit Hilfe eines einfachen Kommandozeilenprogramms ausführen. Das Programm meldet, welche Tests gescheitert sind und wo, so daß die Probleme unmittelbar behoben werden können.

Eine wesentliche Philosophie von erfolgreichen Unit-Tests ist, daß jeder Teiltest mit 100-prozentigem Erfolg beendet werden muß. Wenn ein XSLT-Stylesheet oder eine XML-Datei geändert wird und daraufhin ein Test scheitert, weiß der Entwickler, daß seine Änderungen die Ursache des Problems sind. Bleiben »gescheiterte« Tests zu lange in einem Projekt, werden die Entwickler bald darauf verzichten, das Testprogramm zu benutzen, da es zu aufwendig wird, die Fehlermeldungen manuell zu durchforsten.

Warnung:
Sie sollten eine Strategie durchsetzen, in der jeder Entwickler gezwungen ist, geänderten Code vor dem Einchecken in ein Repository wie CVS durch eine Menge von Unit-Tests auf Fehler zu prüfen.

Ein einfacher Test für XSLT-Transformationen besteht darin, das Ergebnis einer Transformation gegen eine DTD oder ein Schema zu validieren. Nachdem die Struktur des Ergebnisses validiert wurde, können weitere Tests durchgeführt werden, die dessen Inhalt auf semantische Fehler prüfen. Eine DTD kann z.B. feststellen, ob das Element <vorname> vorhanden ist, es werden aber weitere Tests benötigt um festzustellen, ob der Inhalt von <vorname> tatsächlich der korrekte Name ist.

Ein Beispiel für einen Unit-Test

Das folgende Beispiel zeigt, wie eine einfache Test-Fixture mit dem JUnit-Framework erstellt wird. JUnit beschreibt Fixtures als eine Gruppe von Unit-Tests.

Beispiel: Beispiel einer Test-Fixture

package chap9;

import java.io.*;
import java.net.*;
import java.util.*;

// JAXP wird für die XSLT-Transformationen verwendet
import javax.xml.transform.*;
import javax.xml.transform.stream.*;

// JDOM wird für das Parsen und Validieren der XML-Daten verwendet
import org.jdom.*;
import org.jdom.input.*;

// Klassen von JUnit
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.textui.TestRunner;

/**
 * Ein Beispiel für einen JUnit-Test. Diese Klasse führt eine
 * XSLT-Transformation durch und validiert das Ergebnis.
 */
public class SampleUnitTest extends TestCase {
  private String workingDir;
  
  // XML-Eingabedateien
  private File aidanXMLFile;
  private File johnXMLFile;
  
  // ein Stylesheet, das die XML-Daten verdichtet
  private File condenseXSLTFile;
  
  // die Ergebnisse der Transformationen
  private File aidanCondensedXMLFile;
  private File johnCondensedXMLFile;
  
  private TransformerFactory transFact;
  
  /**
   * JUnit-Tests besitzen einen Konstruktor, der als Parameter den Testnamen erhält.
   */
  public SampleUnitTest(String name) {
    super(name);
  }
  
  /**
   * Initialisierung, bevor eine der test[...]-Methoden aufgerufen wird.
   */
  public void setUp( ) {
    // suche die Datei test.properties im Paket chap9
    ResourceBundle rb = ResourceBundle.getBundle("chap9.test");
    this.workingDir = rb.getString("chap9.workingDir");
    
    assertNotNull(workingDir);
    assert("Kann Datei nicht finden: " + this.workingDir,
      new File(this.workingDir).exists( ));
    
    this.aidanXMLFile = new File(workingDir + File.separator
      + "aidan.xml");
    this.johnXMLFile = new File(workingDir + File.separator
      + "john.xml");
    this.condenseXSLTFile = new File(workingDir + File.separator
      + "condensePerson.xslt");
    
    this.aidanCondensedXMLFile = new File(this.workingDir + File.separator
      + "aidanCondensed.xml");
    this.johnCondensedXMLFile = new File(this.workingDir + File.separator
      + "johnCondensed.xml");
    
    this.transFact = TransformerFactory.newInstance( );
  }
  
  /**
   * Säuberungen nach jeder test[...]-Methode
   */
  public void tearDown( ) {
    // die Transformationsergebnisse könnten hier gelöscht werden,
    // der Code hierfür ist aber absichtlich auskommentiert,
    // damit die erzeugten Dateien begutachtet werden können:
    // this.aidanCondensedXMLFile.delete( );
    // this.johnCondensedXMLFile.delete( );
  }
  
  /**
   * Ein einzelner Unit-Test.
   */
  public void testTransformWithTemplates( ) throws Exception {
    Templates templates = this.transFact.newTemplates(
      new StreamSource(this.condenseXSLTFile));
    
    Transformer trans = templates.newTransformer( );
    
    // führe zwei Transformationen mit identischem Transformer durch
    trans.transform(new StreamSource(this.aidanXMLFile),
      new StreamResult(this.aidanCondensedXMLFile));
    trans.transform(new StreamSource(this.johnXMLFile),
      new StreamResult(this.johnCondensedXMLFile));
    
    // validiere die beiden Dateien
    validateCondensedFile(this.aidanCondensedXMLFile,
      "Aidan Garrett Burke", "25.6.1999");
    validateCondensedFile(this.johnCondensedXMLFile,
      "John Emanuel Schmidt", "29.5.1917");
  }
  /**
   * Ein weiterer Unit-Test.
   */
  public void testTransformer( ) throws Exception {
    Transformer trans = this.transFact.newTransformer(
      new StreamSource(this.condenseXSLTFile));
    
    trans.transform(new StreamSource(this.aidanXMLFile),
      new StreamResult(this.aidanCondensedXMLFile));
    
    validateCondensedFile(this.aidanCondensedXMLFile,
      "Aidan Garrett Burke", "6/25/1999");
  }
  
  // eine Hilfsmethode, die von den Unit-Tests verwendet wird
  private void validateCondensedFile(File file, String expectedName,
      String expectedBirthDate) {
    try {
      // führe eine einfache Validierung gegen die DTD durch
      SAXBuilder builder = new SAXBuilder(true); // Validierung
      Document doc = builder.build(file);
      
      // führe weitere Prüfungen durch
      Element nameElem = doc.getRootElement( ).getChild("name");
      assertEquals("Name war nicht korrekt",
        expectedName, nameElem.getText( ));
      
      Element birthDateElem = doc.getRootElement( ).getChild("geburtstag");
      assertEquals("Geburtsdatum war nicht korrekt",
        expectedBirthDate, birthDateElem.getText( ));
      
    } catch (JDOMException jde) {
      fail("XML-Daten waren nicht gültig: " + jde.getMessage( ));
    }
  }
 /**
  * @return eine TestSuite, die eine Zusammensetzung von Testobjekten ist.
  */
  public static Test suite( ) {
    // verwendet die Reflection-API, um alle Methoden mit Namen test[...]
    // zu finden
    return new TestSuite(SampleUnitTest.class);
  }
  
  /**
   * Erlaubt die Ausführung der Unit-Tests im Textmodus von der Kommandozeile.
   */
  public static void main(String[] args) {
    TestRunner.run(suite( ));
  }
}

Die Klasse SampleUnitTest ist eine Ableitung von junit.framework.TestCase. Jede Unterklasse von TestCase definiert eine Fixture und kann mehrere einzelne Unit-Tests enthalten. Jede mit dem Wort »test« beginnende Methode ist ein Unit-Test. Alle privaten Elemente von SampleUnitTest sind nicht Teil des JUnit-Frameworks, sondern auf unsere speziellen Bedürfnisse zugeschnitten.

Der Konstruktor übernimmt den Namen eines Unit-Tests als Argument:

public SampleUnitTest(String name) {
  super(name);
}

Das Argument name ist der Name der Methode, und JUnit verwendet die Reflection-API von Java, um die richtige Methode zu finden und zu instantiieren. Wie wir gleich sehen werden, wird der Konstruktor nur selten direkt aufgerufen.

Die Methode setUp( ) wird vor der Ausführung jedes Unit-Tests aufgerufen. Diese Methode stellt vor der Ausführung eines Tests die nötigen Vorbedingungen ein. Das entsprechende Gegenstück ist die Methode tearDown( ), die nach jedem Test aufgerufen wird. Enthält eine Fixture z.B. vier Unit-Test-Methoden, werden setUp( ) und tearDown( ) jeweils viermal aufgerufen.

In unserem Fall lokalisiert die Methode setUp( ) alle Dateien, die für die XSLT-Transformationen benötigt werden, also XML-Eingabedateien, das XSLT-Stylesheet und die gewünschten Transformationsergebnisse. Es werden auch einfache Tests durchgeführt:

assertNotNull(workingDir);
assert("Kann nicht finden: " + this.workingDir,
    new File(this.workingDir).exists( ));

Die assert( )-Methoden sind Teil des JUnit-Frameworks, sie sorgen dafür, daß ein Test fehlschlägt, wenn die getestete Bedingung nicht erfüllt ist. (In JUnit 3.7 wurde die Methode assert( ) in assertTrue( ) umbenannt, um Konflikte mit dem assert-Mechanismus von JDK 1.4 zu vermeiden.) Es handelt sich um wichtige Methoden beim Erstellen von Unit-Tests, die sowohl in den Testmethoden als auch in den Methoden setUp( ) und tearDown( )verwendet werden können. Ist eine assert-Anweisung nicht erfüllt, liefert JUnit eine Fehlermeldung mit der Zeilennummer, in der die Störung auftrat. Es handelt sich also um eine Teststörung, im Gegensatz zu einem Testfehler. Ein Fehler tritt dann auf, wenn JUnit eine Ausnahme abfängt, die in einem Unit-Test ausgelöst wurde.

Der erste Unit-Test unseres Beispiels ist die Methode testTransformWithTemplates( ). Da sie mit »test« beginnt, kann JUnit den Reflection-Mechanismus verwenden, um sie zu lokalisieren. Die Aufgabe dieses Tests besteht darin, eine XSLT-Transformation mit dem Templates-Interface von JAXP durchzuführen und das Ergebnis an die Methode validateCondensedFile( ) weiterzureichen, die die eigentlichen Tests durchführt. Dieser Ansatz ermöglicht es, denselben Code in einer Gruppe von einzelnen Unit-Tests wiederzuverwenden.

Die Methode validateCondensedFile( ) führt einen zweistufigen Test durch. Als erstes wird das Ergebnis der Transformation mit unserer DTD validiert. Falls hier eine Ausnahme erzeugt wird, schlägt der Test fehl:

fail("XML-Daten waren nicht gültig: " + jde.getMessage( ));

JUnit fängt diese Störung ab und zeigt eine entsprechende Fehlermeldung. Ist die Validierung erfolgreich, verwendet der Unit-Test im zweiten Schritt die Methode assertEquals( ), um den Inhalt der XML-Daten zu prüfen:

assertEquals("Name war nicht korrekt",
      expectedName, nameElem.getText( ));

Sind in dieser Methode das zweite und dritte Argument nicht gleich, wird eine Fehlermeldung ausgegeben, und der Test schlägt fehl.

Eine weitere wichtige Methode ist suite( ):

public static Test suite( ) {
   // verwendet die Reflection-API, um alle Methoden mit Namen test[...]
   // zu finden
   return new TestSuite(SampleUnitTest.class);
}

Sie lokalisiert automatisch alle Methoden, deren Name mit »test« beginnt, und fügt sie zu einer Test-Suite zusammen. Sowohl TestCase als auch TestSuite implementieren das Interface Test, wobei TestSuite eine Zusammenstellung mehrerer einzelner Test-Objekte ist. Indem die Tests zu Suites zusammengefaßt werden, können durch das Starten einer Suite ganze Testfamilien ausgeführt werden. Natürlich können Test-Suites auch andere Test-Suites enthalten. Schließlich kann auf der höchsten Ebene eine Test-Suite alle anderen Tests der Anwendung direkt oder indirekt enthalten, so daß alle Tests mit einem einzigen Kommando ausgeführt werden können.

Das Ausführen des Tests

Um den Test von der Kommandozeile zu starten, geben Sie das folgende Kommando ein:

java chap9.SampleUnitTest

Das funktioniert, da unsere Fixture die Methode main( ) enthält:

public static void main(String[] args) {
  TestRunner.run(suite( ));
}

Die Klasse TestRunner ist ein Kommandozeilen-Tool, das die folgende Ausgabe liefert, wenn alle Tests erfolgreich waren:

Time: 1.081

OK (2 tests)

Die Punkte in der ersten Zeile der Ausgabe repräsentieren die Testmethoden. Bei der Ausführung eines neuen Tests erscheint jeweils ein weiterer Punkt. Schlägt ein Test fehl, gibt JUnit einen Stack-Trace aus, eine (manchmal) anschauliche Fehlermeldung und die Zeile, in der der Fehler auftrat. Am Ende wird die Anzahl von Tests, Störungen und Fehlern ausgegeben.

JUnit besitzt auch einen Swing-GUI-Client, der mit dem folgenden Kommando gestartet werden kann:

java junit.swingui.TestRunner chap9.SampleUnitTest

Die folgende Abbildung zeigt die graphische Ausgabe im Fehlerfall.

JUnit-Ausgabe mit Fehlern

Abbildung: JUnit-Ausgabe mit Fehlern

Bei der rechteckigen Fläche links vom »U« handelt es sich um eine Fortschrittsleiste, die das Ausführen der Tests anzeigt. Handelt es sich um hunderte Tests, gibt sie einen guten Eindruck, wie viele Tests bereits ausgeführt wurden. Die Farbe ändert sich auch von grün nach rot, wenn Fehler oder Störungen auftreten. Die scrollbare Liste in der Mitte zeigt einzelne Testfehler und Störungen, während das untere Textfeld jeweils die Details zu einem ausgewählten Fehler darstellt.

Das GUI-Interface ist gut für interaktive Tests geeignet, während die Kommandozeilenversion automatisierte Tests im Batch-Betrieb ermöglicht. Diese Art von Tests wird während der sogenannten »Nightly-Build«-Prozesse durchgeführt. Wir wenden uns nun von der Software für Unit-Tests zum Gebiet der individuellen Fehlerbehandlung für Anwendungen mit den Error-Listenern von JAXP zu.

Die Error-Listener von JAXP 1.1

Beim Ausführen von XSLT-Transformationen mit JAXP werden Fehler normalerweise nach System.err geschrieben. Während dies für Transformationen auf der Kommandozeile ausreicht, benötigen einige Anwendungen eine bessere Kontrolle über die Fehlerausgabe. Für solche Fälle steht das Interface javax.xml.transform.ErrorListener zur Verfügung.

Indem dieses Interface implementiert wird, können Anwendungen Transformationsfehler abfangen und detaillierte Informationen über Ursache und Ort des Fehlers bereitstellen. Im folgenden Beispiel wird ein Swing-Table-Modell vorgestellt. Diese Klasse implementiert das Interface javax.xml.transform.ErrorListener und wird von einem JTable verwendet, um Fehler graphisch darzustellen. Im Beispiel TransformerWindow.java zeigen wir dann, wie dieser Error-Listener einer TransformerFactory und einem Transformer zugeordnet werden kann.

Beispiel: ErrorListenerModel

package com.oreilly.javaxslt.swingtrans;

import java.io.*;
import java.util.*;
import javax.swing.table.*;

// Import-Anweisungen für XML
import javax.xml.transform.ErrorListener;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.TransformerException;

/**
 * Das Datenmodell JTable stellt detaillierte Informationen über eine Liste
 * von Objekten des Typs javax.xml.transform.TransformerException bereit.
 */
public class ErrorListenerModel extends AbstractTableModel
    implements ErrorListener {
      
  // Tabellenspalte
  private static final int LINE_COL = 0;
  private static final int COLUMN_COL = 1;
  private static final int PUBLIC_ID_COL = 2;
  private static final int SYSTEM_ID_COL = 3;
  private static final int MESSAGE_AND_LOC_COL = 4;
  private static final int LOCATION_COL = 5;
  private static final int EXCEPTION_COL = 6;
  private static final int CAUSE_COL = 7;
      
  private static final String[] COLUMN_NAMES = {
    "Zeile",
    "Spalte",
    "Public ID",
    "System ID",
    "Meldung & Position",
    "Position",
    "Ausnahme",
    "Ursache"
  };

  // die eigentlichen Daten
  private List exceptionList = null;

  /**
   * @return eine detaillierte Textmeldung der Ausnahme in der angegebenen Zeile.
   */
  public String getDetailReport(int row) {
    if (this.exceptionList == null
        || row < 0 || row >= this.exceptionList.size( )) {
      return "";
    }
    TransformerException te = (TransformerException)
      this.exceptionList.get(row);
    SourceLocator loc = te.getLocator( ); // könnte null sein

    // den Report puffern
    StringWriter sw = new StringWriter( );
    PrintWriter pw = new PrintWriter(sw);

    pw.println(te.getClass().getName( ));
    pw.println("-----------------------------------------------------");
    if (loc == null) {
      pw.println("Zeile : [null SourceLocator]");
      pw.println("Spalte : [null SourceLocator]");
      pw.println("Public-ID : [null SourceLocator]");
      pw.println("System-ID : [null SourceLocator]");
    } else {
      pw.println("Zeile : " + loc.getLineNumber( ));
      pw.println("Spalte : " + loc.getColumnNumber( ));
      pw.println("Public-ID : " + loc.getPublicId( ));
      pw.println("System-ID : " + loc.getSystemId( ));
    }

    pw.println("Meldung & Position : " + te.getMessageAndLocation( ));
    pw.println("Position : " + te.getLocationAsString( ));

    pw.println("Ausnahme : " + te.getException( ));
    if (te.getException( ) != null) {
      te.getException( ).printStackTrace(pw);
    }

    pw.println("Ursache : " + te.getCause( ));
    if (te.getCause() != null && (te.getCause() != te.getException( ))) {
      te.getCause( ).printStackTrace(pw);
    }

    return sw.toString( );
  }
  /**
   * Ein Teil des Interface TableModel.
   */
  public Object getValueAt(int row, int column) {
    if (this.exceptionList == null) {
      return "Keine Fehler oder Warnungen";
    } else {
      TransformerException te = (TransformerException)
        this.exceptionList.get(row);
      SourceLocator loc = te.getLocator( );
      
      switch (column) {
      case LINE_COL:
        return (loc != null)
          ? String.valueOf(loc.getLineNumber( )) : "N/A";
      case COLUMN_COL:
        return (loc != null)
          ? String.valueOf(loc.getColumnNumber( )) : "N/A";
      case PUBLIC_ID_COL:
        return (loc != null) ? loc.getPublicId( ) : "N/A";
      case SYSTEM_ID_COL:
        return (loc != null) ? loc.getSystemId( ) : "N/A";
      case MESSAGE_AND_LOC_COL:
        return te.getMessageAndLocation( );
      case LOCATION_COL:
        return te.getLocationAsString( );
      case EXCEPTION_COL:
        return te.getException( );
      case CAUSE_COL:
        return te.getCause( );
      default:
        return "[Fehler]"; // sollte nicht auftreten
      }
    }
  }

  /**
   * Ein Teil des Interface TableModel.
   */
  public int getRowCount( ) {
    return (this.exceptionList == null) ? 1 :
      this.exceptionList.size( );
  }

  /**
    * Ein Teil des Interface TableModel.
    */
  public int getColumnCount( ) {
    return (this.exceptionList == null) ? 1 :
      COLUMN_NAMES.length;
  }

  /**
   * Ein Teil des Interface TableModel.
   */
  public String getColumnName(int column) {
    return (this.exceptionList == null)
      ? "Probleme bei der Transformation"
      : COLUMN_NAMES[column];
  }

  /**
   * @return true, wenn ein Fehler auftritt.
   */
  public boolean hasErrors( ) {
    return this.exceptionList != null;
  }

  /**
   * Das ist ein Teil des Interface javax.xml.transform.ErrorListener.
   * Zeigt an, daß eine Warnung aufgetreten ist. Transformer müssen nach
   * einer Warnung weiterarbeiten, außer wenn die Anwendung eine
   * TransformerException erzeugt.
   */
  public void warning(TransformerException te) throws TransformerException {
    report(te);
  }

  /**
   * Das ist ein Teil des Interface javax.xml.transform.ErrorListener.
   * Zeigt an, daß ein behebbarer Fehler aufgetreten ist.
   */
  public void error(TransformerException te) throws TransformerException {
    report(te);
  }

  /**
   * Das ist ein Teil des Interface javax.xml.transform.ErrorListener.
   * Zeigt an, daß ein nicht-behebbarer Fehler aufgetreten ist.
   */
  public void fatalError(TransformerException te) throws TransformerException {
    report(te);
  }

  // Hängt die Ausnahme an die exceptionList an und benachrichtigt JTable,
  // daß sich der Inhalt der Tabelle geändert hat.
  private void report(TransformerException te) {
    if (this.exceptionList == null) {
      this.exceptionList = new ArrayList( );
      this.exceptionList.add(te);
      fireTableStructureChanged( );
    } else {
      this.exceptionList.add(te);
      int row = this.exceptionList.size( )-1;
      super.fireTableRowsInserted(row, row);
    }
  }
}

Code, der sich auf das Interface ErrorListener bezieht, ist hervorgehoben, der restliche Code stellt die Fehler in einer Swing-Tabelle dar. Die Swing-Komponente JTable visualisiert Daten in Zeilen und Spalten, wobei die Daten aus dem zugrundeliegenden Interface javax.swing.table.TableModel bezogen werden. Die abstrakte Klasse javax.swing.table.AbstractTableModel implementiert TableModel und dient wie in diesem Beispiel als Basisklasse für anwendungsbezogene Table-Models. Wie Sie sehen, ist die Klasse ErrorListenerModel von AbstractTableModel abgeleitet worden.

Da unser Table-Model das Interface ErrorListener implementiert, kann es einem JAXP-Transformer zugeordnet werden. Bei Problemen während der Transformation werden warning( ), error( ) oder fatalError( ) aufgerufen. Da diese Methoden dieselbe Signatur besitzen, rufen sie alle die Methode report( ) auf. Die Kommentare im Code verdeutlichen, welche Probleme zum Aufruf einer bestimmten Methode führen. Allerdings sind die verschiedenen XSLT-Prozessoren nicht konsistent in der Art und Weise ihrer Fehlermeldungen.

Die Methode report( ) fügt ein Objekt des Typs TransformerException einer privaten Liste von Ausnahmen hinzu und löst ein Swing-Event aus, damit die JTable ihren Inhalt neu zeichnet. Wenn die JTable das Event erhält, erfragt es beim ErrorListenerModel die Zeilennummer, die Spaltennummer und die Werte an bestimmten Stellen des Table-Models. Diese Funktionalität befindet sich in den Methoden getRowCount( ), get-ColumnCount( ) und getValueAt( ), die alle im Interface TableModel deklariert werden.

Unsere Klasse besitzt eine weitere Methode namens getDetailReport( ), mit der eine Textmeldung für ein TransformerException -Objekt erzeugt wird. Diese Methode ist deshalb interessant, weil sie verdeutlicht, welche Methoden im Kontext von Transformationsproblemen verfügbar sind. Wie der Code des Beispiels ErrorListenerModel zeigt, können viele der Felder null sein. Einige XSLT-Prozessoren könnten eine Menge detaillierter Fehlermeldungen liefern, während andere einfach null zurückgeben.

Eine GUI für XSLT-Transformationen

In diesem Abschnitt wird eine GUI für XSLT-Transformationen entwickelt. Es handelt sich um eine einfache Swing-Anwendung, die es erlaubt, eine XML-Datei mit Hilfe eines XSLT-Stylesheets zu transformieren. Das Ergebnis der Transformation wird in einem Textfeld innerhalb eines JTable dargestellt. Außerdem werden alle Fehler der Transformation mit Hilfe der Klasse ErrorListenerModel aus dem Beispiel ErrorListenerModel angezeigt.

Die XML-Daten des Transformationsergebnisses können auch validiert werden. Vorausgesetzt, das Stylesheet erzeugt überhaupt XML, versucht dieses Werkzeug, das Ergebnis zu parsen und zu validieren. Sie können z.B. sicherstellen, daß Ihr Stylesheet gültige XHTML-Daten erzeugt, indem Sie das Ergebnis der Transformation gegen eine der XHTML-DTDs validieren.

Die Ant Build-Datei aus dem Beispiel build.xml enthält das Target »run«, mit dem die Anwendung einfach durch die Eingabe von ant run gestartet werden kann.

Anwendungsfenster

Das erste Fenster der Anwendung ist in der folgenden Abbildung zu sehen. Es wird immer angezeigt und erlaubt dem Nutzer, die XML-Eingabedatei und das XSLT-Stylesheet auszuwählen. (Die Bilder zeigen das Look-And-Feel von Java-Swing unter Linux.)

Fenster des SwingTransformers

Abbildung: Fenster des SwingTransformers

Bei Betätigung des Transformieren-Buttons erscheint das Fenster aus der folgenden Abbildung. Es können weitere Transformationen durchgeführt werden, wobei bei jeder ein neues Fenster geöffnet wird. Da die XML- und XSLT-Dateien bei jeder Transformation neu gelesen werden, muß die Anwendung bei Änderungen dieser Dateien nicht neu gestartet werden.

Panel für die XML-Ausgabe

Abbildung: Panel für die XML-Ausgabe

Der Tab Textausgabe wird immer zuerst dargestellt. Er wird hier nicht gezeigt, da er das Transformationsergebnis nur als Text darstellt und somit allen bei der Transformation erzeugten Whitespace enthält. Wenn der Nutzer den Tab XML-Ausgabe anwählt, wird das Transformationsergebnis geparst und gegen eine DTD validiert. Die XML-Daten werden dann mit Hilfe der Klasse XMLOutputter von JDOM dargestellt, die allen unwesentlichen Whitespace entfernt und die Ausgabe ordentlich formatiert.

Treten während der Transformation Fehler auf, bleiben das Text- und das XML-Ausgabepanel leer. Der Nutzer sieht dann das Fenster aus der folgenden Abbildung.

Anzeige bei Problemen während der Transformation

Abbildung: Anzeige bei Problemen während der Transformation

Die Anzeige verdeutlicht, wie das ErrorListenerModel aus dem Beispiel ErrorListenerModel verwendet wird. Die obere JTable zeigt einen tabellarischen Überblick aller aufgetretenen Fehler, während das Textfeld darunter die Ausgabe der Methode getDetailReport( ) von ErrorListenerModel darstellt. In dem gezeigten Beispiel wurde das Attribut select im XSLT-Stylesheet absichtlich als seelect geschrieben.

Quellcode

Der Quellcode des Hauptfensters ist im folgenden Beispiel zu sehen. Es handelt sich um eine Unterklasse von JFrame, mit der XML- und XSLT-Dateinamen ausgewählt werden können. Der Code ist fast ausschließlich GUI-bezogen und wird hier nicht weiter diskutiert.

Beispiel: SwingTransformer.java

package com.oreilly.javaxslt.swingtrans;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;

/**
 * Die Hauptklasse der Anwendung. Diese Klasse zeigt das Hauptfenster
 * an, in dem der Nutzer XML- und XSLT-Datei wählen kann.
 */
public class SwingTransformer extends JFrame {
  private JTextField xmlFileFld = new JTextField(30);
  private JTextField xsltFileFld = new JTextField(30);
  
  // Dateifilter, die in der Klasse JFileChooser verwendet werden
  private XMLFileFilter xmlFilter = new XMLFileFilter( );
  private XSLTFileFilter xsltFilter = new XSLTFileFilter( );
  private JFileChooser fileChooser = new JFileChooser( );
  
  // Aktionen werden mit JButtons verknüpft
  private Action loadXMLAction =
      new javax.swing.AbstractAction("OK") {
    public void actionPerformed(ActionEvent evt) {
      selectXMLFile( );
    }
  };
  
  private Action loadXSLTAction =
      new javax.swing.AbstractAction("OK") {
    public void actionPerformed(ActionEvent evt) {
      selectXSLTFile( );
    }
  };
  
  private Action transformAction =
      new javax.swing.AbstractAction("Transformieren") {
    public void actionPerformed(ActionEvent evt) {
      File xmlFile = new File(xmlFileFld.getText( ));
      File xsltFile = new File(xsltFileFld.getText( ));
      
      if (!xmlFile.exists() || !xmlFile.canRead( )) {
        showErrorDialog("Kann XML-Datei nicht lesen");
        return;
      }
      
      if (!xsltFile.exists() || !xsltFile.canRead( )) {
        showErrorDialog("Kann XSLT-Datei nicht lesen");
        return;
      }
      
      // stelle das Transformationsergebnis in einem neuen Fenster dar
      new TransformerWindow( ).transform(xmlFile, xsltFile);
    }
  };
  
  /**
   * Der Startpunkt der Anwendung; zeigt das Hauptfenster.
   */
  public static void main(String[] args) {
    new SwingTransformer( ).setVisible(true);
  }
  
  /**
   * Erzeuge das Hauptfenster und das Layout der GUI.
   */
  public SwingTransformer( ) {
    super("Swing XSLT-Transformation");
    
    // Bemerkung: diese Zeile benötigt Java 2 v1.3
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
    Container cp = getContentPane( );
    cp.setLayout(new GridBagLayout( ));
    
    GridBagConstraints gbc = new GridBagConstraints( );
    gbc.anchor = GridBagConstraints.WEST;
    gbc.fill = GridBagConstraints.HORIZONTAL;
    gbc.gridx = GridBagConstraints.RELATIVE;
    gbc.gridy = 0;
    gbc.insets.top = 2;
    gbc.insets.left = 2;
    gbc.insets.right = 2;
    
    cp.add(new JLabel("XML-Datei:"), gbc);
    gbc.weightx = 1.0;
    cp.add(this.xmlFileFld, gbc);
    gbc.weightx = 0.0;
    cp.add(new JButton(this.loadXMLAction), gbc);
    
    gbc.gridy++;
    cp.add(new JLabel("XSLT-Stylesheet:"), gbc);
    gbc.weightx = 1.0;
    cp.add(this.xsltFileFld, gbc);
    gbc.weightx = 0.0;
    cp.add(new JButton(this.loadXSLTAction), gbc);
    
    gbc.gridy++;
    gbc.gridx = 0;
    gbc.gridwidth = GridBagConstraints.REMAINDER;
    gbc.anchor = GridBagConstraints.CENTER;
    gbc.fill = GridBagConstraints.NONE;
    cp.add(new JButton(this.transformAction), gbc);
    pack( );
  }
  
  /**
   * Zeige den Dateidialog mit allen XML-Dateien.
   */
  private void selectXMLFile( ) {
    this.fileChooser.setDialogTitle("Wählen Sie eine XML-Datei");
    this.fileChooser.setFileFilter(this.xmlFilter);
    int retVal = this.fileChooser.showOpenDialog(this);
    if (retVal == JFileChooser.APPROVE_OPTION) {
      this.xmlFileFld.setText(
        this.fileChooser.getSelectedFile().getAbsolutePath( ));
    }
  }

  /**
   * Zeige den Dateidialog mit allen XSLT-Dateien.
   */
  private void selectXSLTFile( ) {
    this.fileChooser.setDialogTitle("Wählen Sie ein XSLT-Stylesheet");
    this.fileChooser.setFileFilter(this.xsltFilter);
    int retVal = this.fileChooser.showOpenDialog(this);
    if (retVal == JFileChooser.APPROVE_OPTION) {
      this.xsltFileFld.setText(
        this.fileChooser.getSelectedFile().getAbsolutePath( ));
    }
  }

  private void showErrorDialog(String msg) {
    JOptionPane.showMessageDialog(this, msg, "Fehler",
      JOptionPane.ERROR_MESSAGE);
  }
}

/**
 * Wird im JFileChooser verwendet, um nur die Endungen .xml und .XML zuzulassen.
 */
class XMLFileFilter extends javax.swing.filechooser.FileFilter {
  public boolean accept(File f) {
    String name = f.getName( );
    return f.isDirectory( ) || name.endsWith(".xml")
      || name.endsWith(".XML");
  }
  
  public String getDescription( ) {
    return "XML-Dateien";
  }
}

/**
 * Wird im JFileChooser verwendet, um nur die Endungen .xsl, .XSL, .xslt und .XSLT
 * zuzulassen.
 */
class XSLTFileFilter extends javax.swing.filechooser.FileFilter {
  public boolean accept(File f) {
    String name = f.getName( );
    return f.isDirectory( ) || name.endsWith(".xsl")
      || name.endsWith(".xslt") || name.endsWith(".XSL")
      || name.endsWith(".XSLT");
  }
  
  public String getDescription( ) {
    return "XSLT-Stylesheet";
  }
}

Die nächste Klasse, aus dem folgenden Beispiel, erzeugt die Fenster, die in den Abbildungen Fenster des SwingTransformers und Panel für die XML-Ausgabe zu sehen sind. Ein großer Teil des Codes sorgt für die Formatierung der Komponente JTabbedPane mit ihren drei Tabs. Die Klasse führt auch die eigentliche XSLT-Transformation durch.

Beispiel: TransformerWindow.java

package com.oreilly.javaxslt.swingtrans;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.*;

// import-Anweisungen in bezug auf XML
import javax.xml.transform.SourceLocator;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

/**
 * Ein sekundärer JFrame, der das Ergebnis einer einzelnen XSLT-
 * Transformation anzeigt. Es wird das Interface JTabbedPane verwendet,
 * um Transformationsergebnis, Fehlermeldungen und XML-Ausgabe darzustellen.
 */
public class TransformerWindow extends JFrame {
  // das Ergebnis der XSLT-Transformation als Text
  private String resultText;
  
  private JTabbedPane tabPane = new JTabbedPane( );
  private JTextArea textOutputArea = new JTextArea(30, 70);
  private XMLOutputPanel xmlOutputPanel = new XMLOutputPanel( );
  private ErrorListenerModel errModel = new ErrorListenerModel( );
  private JTable errorTable = new JTable(this.errModel);
  private JTextArea errorDetailArea = new JTextArea(10, 70);
  private String xsltURL;
  
  /**
   * Erzeuge eine neue Instanz und ordne die GUI-Komponenten an.
   */
  public TransformerWindow( ) {
    super("XSLT-Transformation");
    
    // füge den Tab-Pane hinzu
    Container cp = getContentPane( );
    cp.add(this.tabPane, BorderLayout.CENTER);
    
    // füge die einzelnen Tabs hinzu
    this.tabPane.add("Textausgabe", new JScrollPane(this.textOutputArea));
    this.tabPane.add("Transformations-Probleme",
      createErrorPanel( ));
    this.tabPane.add("XML-Ausgabe", this.xmlOutputPanel);
    
    // achte auf Auswahländerungen der Tabs
    this.tabPane.addChangeListener(new ChangeListener( ) {
      public void stateChanged(ChangeEvent evt) {
        tabChanged( );
      }
    });
    
    this.textOutputArea.setEditable(false);
    
    // achte auf Auswahländerungen in der Fehlertabelle
    this.errorTable.getSelectionModel( ).addListSelectionListener(
      new ListSelectionListener( ) {
        public void valueChanged(ListSelectionEvent evt) {
          if (!evt.getValueIsAdjusting( )) {
            showErrorDetails( );
          }
        }
      });
    pack( );
  }
  
  /**
   * Zeige die Details zum aktuell ausgewählten Fehler.
   */
  private void showErrorDetails( ) {
    int selRow = this.errorTable.getSelectedRow( );
    this.errorDetailArea.setText(this.errModel.getDetailReport(selRow));
  }
  
  /**
   * Führe die XSLT-Transformation durch.
   */
  public void transform(File xmlFile, File xsltFile) {
    setVisible(true);
    try {
      // Ermittle das Verzeichnis der XSLT-Datei. Dort wird
      // auch nach der DTD gesucht
      if (xsltFile != null) {
        File xsltDir = xsltFile.getParentFile( );
        if (xsltDir.isDirectory( )) {
          this.xsltURL = xsltDir.toURL().toExternalForm( );
        }
      }
      
      TransformerFactory transFact = TransformerFactory.newInstance( );
      
      // registriere das TableModel als Error-Listener
      transFact.setErrorListener(this.errModel);
      
      Transformer trans = transFact.newTransformer(
        new StreamSource(xsltFile));
      
      // Prüfe auf null, weil die Factory evtl. keine Ausnahme
      // auslöst, wenn der Aufruf von newTransformer( ) fehlschlägt.
      // Das ist nötig, da wir einen Error-Listener angemeldet haben,
      // der keine Ausnahmen auslöst.
      if (trans != null) {
        trans.setErrorListener(this.errModel);
       
        // hole das Ergebnis der XSLT-Transformation
        StringWriter sw = new StringWriter( );
        trans.transform(new StreamSource(xmlFile),
          new StreamResult(sw));
       
        // visualisiere das Ergebnis
        this.resultText = sw.toString( );
        this.textOutputArea.setText(this.resultText);
      }
      
    } catch (TransformerConfigurationException tce) {
      try {
        this.errModel.fatalError(tce);
      } catch (TransformerException ignored) {
    }
    } catch (TransformerException te) {
      try {
        this.errModel.fatalError(te);
      } catch (TransformerException ignored) {
    }
    } catch (Exception unexpected) {
      System.err.println(
        "Der XSLT-Prozessor hat eine unerwartete Ausnahme erzeugt");
      unexpected.printStackTrace( );
    }
    
    // zeige den Fehler-Tab
    if (this.errModel.hasErrors( )) {
      this.tabPane.setSelectedIndex(1);
    }
  }
  
  // der Nutzer hat einen anderen Tab ausgewählt
  private void tabChanged( ) {
    try {
      setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
      int selIndex = this.tabPane.getSelectedIndex( );
      String selTab = this.tabPane.getTitleAt(selIndex);
    
      // Bei Auswahl des XML-Tabs wird der Text im XML-Panel angezeigt.
      // Der Text muß zwar kein XML sein, wir wissen das aber erst,
      // wenn er geparst wurde.
      if ("XML-Ausgabe".equals(selTab)) {
        this.xmlOutputPanel.setXML(this.resultText, this.xsltURL);
      }
    } finally {
      setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    }
  }
  
  // eine Hilfsmethode, die das Panel für die Fehlermeldungen erzeugt
  private JComponent createErrorPanel( ) {
    JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
    this.errorTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    int size = this.errorDetailArea.getFont().getSize( );
    this.errorDetailArea.setEditable(false);
    this.errorDetailArea.setFont(
      new Font("Monospaced", Font.PLAIN, size+2));
  
    splitPane.setTopComponent(new JScrollPane(this.errorTable));
    splitPane.setBottomComponent(new JScrollPane(this.errorDetailArea));
    return splitPane;
  }
}

Der hervorgehobene Code zeigt, wie das Error Listener Table-Modell und die Transformer-Instanz bei der TransformerFactory registriert werden. Zusätzlich zur Installation des Error-Listeners müssen auch Ausnahmen abgefangen werden, da XSLT-Prozessoren trotz Error-Listener Ausnahmen und Fehler erzeugen können. Treten Fehler bei der TransformerFactory auf, ist das auf Probleme beim Parsen des XSLT-Stylesheets zurückzuführen, während Error-Listener für Transformer bei Problemen mit der eigentlichen Transformation der XML-Daten benachrichtigt werden.

Die Klasse XMLOutputPanel ist im folgenden Beispiel zu sehen.

Beispiel: XMLOutputPanel.java

package com.oreilly.javaxslt.swingtrans;

import java.awt.*;
import java.io.*;
import javax.swing.*;

// import-Anweisungen in bezug auf XML
import org.jdom.Document;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;

/**
 * Zeigt einen XML-Text in einem scrollbaren Textfeld an. Eine Statuszeile
 * zeigt an, ob die XML-Daten wohlgeformt und gültig sind.
 */
public class XMLOutputPanel extends JPanel {
  // zeigt die XML-Daten an
  private JTextArea xmlArea = new JTextArea(20,70);
  private String xml;
  private JLabel statusLabel = new JLabel( );
  
  /**
   * Erzeuge das Panel und ordne die GUI-Komponenten an.
   */
  public XMLOutputPanel( ) {
    super(new BorderLayout( ));
    add(new JScrollPane(this.xmlArea), BorderLayout.CENTER);
    add(this.statusLabel, BorderLayout.NORTH);
  }
  
  /**
   * @param xml die anzuzeigenden XML-Daten.
   * @param uri die Position der XML-Daten; sie erlauben dem
   * Parser, die DTD zu finden.
   */
  public void setXML(String xml, String uri) {
    // sofort zurückkehren, wenn diese XML-Daten bereits angezeigt werden
    if (xml == null || xml.equals(this.xml)) {
      return;
    }
    this.xml = xml;
  
    // verwende JDOM, um die XML-Daten zu parsen
    Document xmlDoc = null;
    try {
      // versuche die XML-Daten zu validieren
      SAXBuilder saxBuilder = new SAXBuilder(true);
      xmlDoc = saxBuilder.build(new StringReader(this.xml), uri);
      this.statusLabel.setText("XML ist wohlgeformt und gültig");
    } catch (Exception ignored) {
      // die Daten sind zwar nicht gültig, wir sollten sie aber
      // noch einmal parsen, um festzustellen, ob sie wohlgeformt sind
    }
  
    if (xmlDoc == null) {
      try {
        // nicht validieren
        SAXBuilder saxBuilder = new SAXBuilder(false);
        xmlDoc = saxBuilder.build(new StringReader(this.xml));
        this.statusLabel.setText("XML ist wohlgeformt, aber nicht gültig");
      } catch (Exception ex) {
        this.statusLabel.setText("XML ist nicht wohlgeformt");
        
        // zeige den Stack-Trace im Textfeld an
        StringWriter sw = new StringWriter( );
        ex.printStackTrace(new PrintWriter(sw));
        this.xmlArea.setText(sw.toString( ));
      }
    }

    // zeige das Dokument an, falls es geparst wurde
    if (xmlDoc != null) {
      try {
        // formatiere die XML-Daten durch Einrückung um zwei Stellen
        XMLOutputter xmlOut = new XMLOutputter(" ", true);
        StringWriter sw = new StringWriter( );
        xmlOut.output(xmlDoc, sw);
        this.xmlArea.setText(sw.toString( ));
      } catch (Exception ex) {
        this.statusLabel.setText("Daten konnten nicht angezeigt werden.");
        
        // zeige den Stack-Trace im Textfeld
        StringWriter sw = new StringWriter( );
        ex.printStackTrace(new PrintWriter(sw));
        this.xmlArea.setText(sw.toString( ));
      }
    }
  }
}

XMLOutputPanel parst das Ergebnis, um herauszufinden, ob es sich um wohlgeformte und gültige XML-Daten handelt. Zuerst wird der Text mit einem validierenden Parser untersucht, wobei Fehler einfach ignoriert werden. Treten keine Fehler auf, handelt es sich um wohlgeformte und gültige XML-Daten, die im Textfeld angezeigt werden können. Ansonsten wird das Dokument noch einmal eingelesen, diesmal aber ohne Validierung. So kann herausgefunden werden, ob die XML-Daten wenigstens wohlgeformt sind.

Ist das Dokument nicht wohlgeformt oder gültig, wird der Stack-Trace des Parsers in der GUI angezeigt. Bei vielen XSLT-Transformationen ist das Ergebnis von vornherein kein XML, so daß diese Nachricht ignoriert werden kann. Treten jedoch Fehler auf, sollte es einfach sein, die Ursache zu finden.

   

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