Erweiterungselemente mit Java hinzufügen

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie möchten die Fähigkeiten von XSLT erweitern, indem Sie Elemente mit eigenem Verhalten hinzufügen.

Lösung

Die vorherigen Abschnitte untersuchten, wie Sie Erweiterungen von den Herstellern einer XSLT-Implementierung vorteilhaft benutzen können. In diesem Abschnitt entwickeln Sie Ihre eigenen Erweiterungselemente von Grund auf selbst. Im Gegensatz zu Erweiterungsfunktionen erfordert die Erstellung von Erweiterungselementen eine wesentlich bessere Kenntnis der Implementierungsdetails zu einem bestimmten Prozessor. Da die verschiedenen Prozessor-Designs sehr unterschiedlich sind, wird ein großer Teil des Codes nicht über mehrere Prozessoren hinweg portabel sein.

Dieser Abschnitt beginnt mit einer einfachen Erweiterung, die eher eine Verschönerung der Syntax darstellt als eine wirklich erweiterte Funktionalität bietet. Beim Kodieren in XSLT muss man häufig den Kontext wechseln und zu einem anderen Knoten springen. Ein Idiom, mit dem man das erreichen kann, ist xsl:for-each. Etwas verwirrend dabei ist, dass die eigentliche Absicht keine Iteration über, sondern ein Wechsel des Kontexts zu jenem einzelnen Knoten ist, der in xsl:for-each durch select angegeben ist:

<xsl:for-each select="document('new.xml')">
  <!-- Bearbeite neues Dokument -->
</xsl:for-each>

Sie werden nun ein Erweiterungselement namens xslx:set-context implementieren, das sich genauso verhält wie xsl:for-each, aber nur auf dem ersten Knoten der Knotenmenge, die im select definiert wird (normalerweise haben Sie sowieso nur einen Knoten).

Saxon verlangt die Implementierung des Interfaces com.icl.saxon.style.ExtensionElementFactory für alle Erweiterungselemente, die mit einem bestimmten Namensraum verbunden sind. Die Factory ist dafür verantwortlich, die Erweiterungselemente aus dem lokalen Namen der Elemente zu erzeugen. Die zweite Erweiterung namens templtext wird später behandelt:

package com.ora.xsltckbk;
import com.icl.saxon.style.ExtensionElementFactory;
import org.xml.sax.SAXException;

public class CkBkElementFactory implements ExtensionElementFactory {

    public Class getExtensionClass(String localname)  {
        if (localname.equals("set-context")) return CkBkSetContext.class;
        if (localname.equals("templtext")) return CkBkTemplText.class;
        return null;
    }

}

Wenn Sie eine Stylesheet-Erweiterung benutzen, dann müssen Sie einen Namensraum verwenden, der mit einem / endet, gefolgt vom vollständig qualifizierten Namen der Factory. Außerdem muss das Namensraumpräfix im Attribut extension-element-prefixes von xsl:stylesheet vorkommen:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xslx="http://com.ora.xsltckbk.CkBkElementFactory" extension-element-prefixes="xslx">
  <xsl:template match="/">
    <xslx:set-context select="foo/bar">
      <xsl:value-of select="."/>
    </xslx:set-context>
  </xsl:template>
</xsl:stylesheet>

Die Implementierung des set-context-Elements ist von com.icl.saxon.style.StyleElement abgeleitet und muss die Methoden prepareAttributes( ) und process( ) implementieren, wird aber normalerweise auch jene weiteren Methoden implementieren, die in der folgenden Tabelle aufgeführt sind.

Tabelle: Wichtige StyleElement-Methoden in Saxon.

Methode Effekt
isInstruction( ) Erweiterung gibt immer true zurück.
mayContainTemplateBody( ) Gibt true zurück, falls dieses Element Kindelemente enthalten darf. Gibt oftmals true zurück, um ein xsl:fallback-Kindelement zu erlauben.
prepareAttributes( ) Wird zum Zeitpunkt des Kompilierens aufgerufen, damit die Klasse die in den Erweiterungsattributen enthaltenen Informationen parsen kann. Das ist auch die Zeit für eine lokale Validierung.
validate( ) Wird zum Zeitpunkt des Kompilierens aufgerufen, nachdem alle Stylesheet-Elemente eine lokale Validierung durchgeführt haben. Sie ermöglicht eine gegenseitige Validierung zwischen diesem Element und seinen Eltern bzw. Kindern.
process(Context context) Wird zur Laufzeit aufgerufen, um die Erweiterung auszuführen. Diese Methode kann auf Informationen im Kontext zugreifen und sie verändern, darf aber nicht den Stylesheet-Baum verändern.

Das xslx:set-context-Element war leicht zu implementieren, weil der Code aus der XSLForEach-Implementierung von Saxon geklaut und so modifiziert wurde, dass er das macht, was XSLForEach macht, aber nur einmal:

public class CkBkSetContext extends com.icl.saxon.style.StyleElement {

    Expression select = null;

    public boolean isInstruction( ) {
        return true;
    }
    public boolean mayContainTemplateBody( ) {
        return true;
    }

Und nun sorgen Sie dafür, dass das @select vorhanden ist. Wenn es vorhanden ist, rufen Sie makeExpression auf, das es in einen XPath-Ausdruck parst:

public void prepareAttributes( )
                  throws TransformerConfigurationException {

      StandardNames sn = getStandardNames( );
      AttributeCollection atts = getAttributeList( );

      String selectAtt = null;

      for (int a=0; a<atts.getLength( ); a++) {
               int nc = atts.getNameCode(a);
           int f = nc & 0xfffff;
           if (f == sn.SELECT) {
              selectAtt = atts.getValue(a);
         } else {
              checkUnknownAttribute(nc);
         }
      }

      if (selectAtt=  =null) {
         reportAbsence("select");
      } else {
          select = makeExpression(selectAtt);
      }
   }

   public void validate( ) throws TransformerConfigurationException {
       checkWithinTemplate( );
   }

Dieser Code ist identisch zu dem von for-each in Saxon, außer, dass er statt einer Iteration über selection.hasMoreElements nur einmal prüft, das Element extrahiert, den Kontext und aktuellen Knoten setzt, Kindelemente bearbeitet und das Ergebnis an den Kontext zurückgibt:

public void process(Context context) throws TransformerException
{
    NodeEnumeration selection = select.enumerate(context, false);
    if (!(selection instanceof LastPositionFinder)) {
        selection = new LookaheadEnumerator(selection);
    }

    Context c = context.newContext( );
    c.setLastPositionFinder((LastPositionFinder)selection);
    int position = 1;

      if (selection.hasMoreElements( )) {
          NodeInfo node = selection.nextElement( );
          c.setPosition(position++);
          c.setCurrentNode(node);
          c.setContextNode(node);
          processChildren(c);
              context.setReturnValue(c.getReturnValue( ));
          }
    }
}

Das nächste Erweiterungsbeispiel ist deswegen nicht so einfach, weil es die Möglichkeiten von XSLT erweitert und nicht nur eine alternative Implementierung für eine vorhandene Funktionalität bietet.

Aus der Tatsache, dass ein ganzes Kapitel in diesem Buch dem Thema Code-Generierung gewidmet ist, können Sie ersehen, dass mich diese Aufgabe interessiert. Aber obwohl XSLT in seinen Möglichkeiten der Manipulation von XML fast optimal ist, fehlen ihm gewisse Ausgabemöglichkeiten, was am Wortreichtum von XML liegt. Betrachten Sie eine einfache Aufgabe für die Code-Generierung mit C++ in nativem XSLT:

<classes>
  <class>
    <name>MyClass1</name>
  </class>
  <class>
    <name>MyClass2</name>
  </class>
  <class>
    <name>MyClass3</name>
    <bases>
      <base>MyClass1</base>
      <base>MyClass2</base>
    </bases>
  </class>
</classes>

Ein Stylesheet, das dieses XML in C++ transformiert, könnte wie folgt aussehen:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:template match="class">
  class <xsl:value-of select="name"/> <xsl:apply-templates select="bases"/>
  {
  public:

    <xsl:value-of select="name"/>( ) ;
    ~<xsl:value-of select="name"/>( ) ;
    <xsl:value-of select="name"/>(const <xsl:value-of select="name"/>&amp; other) ;
    <xsl:value-of select="name"/>&amp; operator =(const <xsl:value-of select="name"/>
    &amp; other) ;
  } ;
  </xsl:template>
  <xsl:template match="bases">
    <xsl:text>: public </xsl:text>
    <xsl:for-each select="base">
      <xsl:value-of select="."/>
      <xsl:if test="position( ) != last( )">
        <xsl:text>, public </xsl:text>
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
  <xsl:template match="text( )"/>
</xsl:stylesheet>

Dieser Code ist mühsam zu schreiben und schwer zu lesen, weil das C++ in einem Wust von Markup-Tags verloren geht.

Die Erweiterung xslx:templtext geht dieses Problem mit Hilfe einer alternativen Implementierung von xsl:text an, die spezielle Escape-Sequenzen enthalten darf, die auf besondere Weise bearbeitet werden. Eine Escape-Sequenz wird durch umgebende Backslashes (\) markiert und kann in zwei Formen vorkommen. Eine offensichtliche Alternative wäre es, {und} zu verwenden, um Attributwert-Templates und XQuery nachzumachen. Aber weil Sie diese Zeichen auch in Code-Generatoren häufig benutzen, habe ich mich für Backslashes entschieden.

Escape-Sequenz Äquivalentes XSLT
\ausdruck\ <xsl:value-of select="ausdruck"/>
\ausdruck%trennzeichen\a <xsl:for-each select="ausdruck">
  <xsl:value-of select="."/>
  <xsl:if test="position( ) != last( )>
    <xsl:value-of select="trennzeichen"/>
  </xsl:if>
</xsl:for-each>

a XSLT 2.0 bietet diese Funktionalität in Form von <xsl:value-of select="ausdruck" separator="trennzeichen" />.

Mit diesem Hilfsmittel sähe Ihr Code-Generator wie folgt aus:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xslx="http://com.ora.xsltckbk.CkBkElementFactory" extension-element-prefixes="xslx">
  <xsl:output method="text"/>
  <xsl:template match="class">
    <xslx:templtext>
    class \name\ <xsl:apply-templates select="bases"/>
    {
    public:

      \name\( ) ;
      ~\name\( ) ;
      \name\(const \name\&amp; other) ;
      \name\&amp; operator =(const \name\&amp; other) ;
    } ;
    </xslx:templtext>
  </xsl:template>
  <xsl:template match="bases">
    <xslx:templtext>: public \base%', public '\</xslx:templtext>
  </xsl:template>
  <xsl:template match="text( )"/>
</xsl:stylesheet>

Diesen Code kann man wesentlich einfacher lesen und schreiben. Dieses Hilfsmittel ist in jedem Kontext anwendbar, in dem viel vorgefertigter Text generiert wird. XSLT-Puristen werden bei solch einer Erweiterung vermutlich die Stirn runzeln, weil sie eine fremde Syntax in XSLT einführt, die nicht mehr einfach als XML manipuliert werden kann. Das ist ein triftiges Argument, aber rein praktisch gesehen würden viele Entwickler XSLT ablehnen (und zu Perl greifen), um vorgefertigte Texte zu generieren, einfach weil es keine prägnante und unaufdringliche Syntax hat, um schnell eine Aufgabe zu erledigen. Also genug mit der Herumdruckserei, schreiben wir einfach den Code hin:

package com.ora.xsltckbk;
import java.util.Vector ;
import java.util.Enumeration ;
import com.icl.saxon.tree.AttributeCollection;
import com.icl.saxon.*;
import com.icl.saxon.expr.*;
import javax.xml.transform.*;
import com.icl.saxon.output.*;
import com.icl.saxon.trace.TraceListener;
import com.icl.saxon.om.NodeInfo;
import com.icl.saxon.om.NodeEnumeration;
import com.icl.saxon.style.StyleElement;
import com.icl.saxon.style.StandardNames;
import com.icl.saxon.tree.AttributeCollection;
import com.icl.saxon.tree.NodeImpl;

Ihre Erweiterungsklasse deklariert zuerst Konstanten, die in einem einfachen endlichen Automaten benutzt werden, der die Escape-Sequenzen parst:

public class CkBkTemplText extends com.icl.saxon.style.StyleElement
{
  private static final int SCANNING_STATE = 0 ;
  private static final int FOUND1_STATE   = 1 ;
  private static final int EXPR_STATE     = 2 ;
  private static final int FOUND2_STATE   = 3 ;
  private static final int DELIMIT_STATE  = 4 ;
...

Dann definieren Sie Ihre privaten Klassen, die die Minisprache im xslx:templtext-Element implementieren. Die Basisklasse, CkBkTemplParam, hält den wörtlichen Text fest, der eventuell vor einer Escape-Sequenz steht:

private class CkBkTemplParam
{
  public CkBkTemplParam(String prefix)
  {
    m_prefix = prefix ;
  }

  public void process(Context context) throws TransformerException
  {
    if (!m_prefix.equals(""))
    {
        Outputter out = context.getOutputter( );
        out.setEscaping(false);
        out.writeContent(m_prefix);
        out.setEscaping(true);
    }
  }

  protected String m_prefix ;
}

Die Klasse CkBkValueTemplParam ist von CkBkTemplParam abgeleitet und implementiert das Verhalten einer einfachen Value-of-Escape-Sequenz \ausdr\. Um die Implementierung in diesem Beispiel zu vereinfachen, wird innerhalb eines xslx:templtext-Elements eine abgeschaltete Escape-Behandlung der Ausgabe zum Normalfall:

private class CkBkValueTemplParam extends CkBkTemplParam
{
  public CkBkValueTemplParam(String prefix, Expression value)
  {
    super(prefix) ;
    m_value = value ;
  }

  public void process(Context context) throws TransformerException
  {
    super.process(context) ;
    Outputter out = context.getOutputter( );
    out.setEscaping(false);
    if (m_value != null)
    {
        m_value.outputStringValue(out, context);
    }
    out.setEscaping(true);
  }

  private Expression m_value ;

}

Die Klasse CkBkTemplParam implementiert das Verhalten von \ausdr%trennzeichen\, indem sie überwiegend das Verhalten einer Saxon-Klasse namens XslForEach imitiert:

private class CkBkListTemplParam extends CkBkTemplParam
{
  public CkBkListTemplParam(String prefix, Expression list, Expression delimit)
  {
    super(prefix) ;
    m_list = list ;
    m_delimit = delimit ;
  }

  public void process(Context context) throws TransformerException
  {
    super.process(context) ;
    if (m_list != null)
    {
      NodeEnumeration m_listEnum = m_list.enumerate(context, false);

      Outputter out = context.getOutputter( );
      out.setEscaping(false);
      while(m_listEnum.hasMoreElements( ))
      {
        NodeInfo node = m_listEnum.nextElement( );
        if (node != null)
        {
          node.copyStringValue(out);
        }
        if (m_listEnum.hasMoreElements( ) && m_delimit != null)
        {
          m_delimit.outputStringValue(out, context);
        }
      }
      out.setEscaping(true);
    }
  }

  private Expression m_list = null;
  private Expression m_delimit = null ;
}

Die letzte private Klasse ist CkBkStyleTemplParam, die zur Aufbewahrung von verschachtelten Elementen in xslx:templtext benutzt wird, z.B. xsl:apply-templates:

private class CkBkStyleTemplParam extends CkBkTemplParam
{
  public CkBkStyleTemplParam(StyleElement snode)
  {
    m_snode = snode ;
  }
  public void process(Context context) throws TransformerException
{
   if (m_snode.validationError != null)
  {
          fallbackProcessing(m_snode, context);
   }
  else
  {
       try
    {
       context.setStaticContext(m_snode.staticContext);
       m_snode.process(context);
     }
    catch (TransformerException err)
    {
       throw snode.styleError(err);
     }
   }
  }
}

Die nächsten drei Methoden sind Standard. Wenn Sie möchten, dass das Standardattribut disable-output-escaping die Ausgabe der Escape-Sequenzen beeinflusst, dann würden Sie dessen Wert in prepareAttributes( ) festhalten. Der Saxon-Quellcode in XslText.java enthält den nötigen Code:

public boolean isInstruction( )
{
    return true;
}

public boolean mayContainTemplateBody( )
{
  return true;
 }

public void prepareAttributes( ) throws TransformerConfigurationException
{
  StandardNames sn = getStandardNames( );
   AttributeCollection atts = getAttributeList( );
   for (int a=0; a<atts.getLength( ); a++)
  {
     int nc = atts.getNameCode(a);
    checkUnknownAttribute(nc);
  }
}

Die Validierungsphase bietet die Gelegenheit, den Inhalt des xslx:templtext-Elements zu parsen, um nach Escape-Sequenzen zu suchen. Sie schicken alle Textknoten an eine Parser-Funktion. Der Stilinhalt des Elements wird in CkBkStyleTemplParam-Instanzen konvertiert. Die Member-Variable m_TemplParms ist ein Vektor, in dem die Parsing-Ergebnisse gespeichert werden:

public void validate( ) throws TransformerConfigurationException
{
    checkWithinTemplate( );
    m_TemplParms = new Vector( ) ;

    NodeImpl node = (NodeImpl)getFirstChild( );
    String value ;
    while (node!=null)
    {
      if (node.getNodeType( ) =  = NodeInfo.TEXT)
      {
        parseTemplText(node.getStringValue( )) ;
      }
      else
      if (node instanceof StyleElement)
      {
         StyleElement snode = (StyleElement) node;
        m_TemplParms.addElement(new CkBkStyleTemplParam(snode)) ;
      }
      node = (NodeImpl)node.getNextSibling( );
    }
 }

Die Methode process iteriert über m_TemplParms und ruft die process-Methode der jeweiligen Implementierung auf:

public void process(Context context) throws TransformerException
{
  Enumeration iter = m_TemplParms.elements( ) ;
  while (iter.hasMoreElements( ))
  {
     CkBkTemplParam param = (CkBkTemplParam) iter.nextElement( ) ;
     param.process(context) ;
  }
}

Die folgenden privaten Funktionen implementieren einen einfachen Parser in Form eines endlichen Automaten, den Sie einfacher implementieren könnten, wenn Sie Zugriff auf eine Engine für reguläre Ausdrücke hätten (die es in Java Version 1.4.1 tatsächlich gibt). Der Parser behandelt zwei Backslashes hintereinander (\\) als einen gewünschten wörtlichen Backslash. Ebenso wird %% in ein einziges % übersetzt:

private void parseTemplText(String value)
{
    // Dieser endliche Automat parst den Text und sucht nach Parametern
    int ii = 0 ;
    int len = value.length( ) ;

    int state = SCANNING_STATE ;
    StringBuffer temp = new StringBuffer("") ;
    StringBuffer expr = new StringBuffer("") ;
    while(ii < len)
    {
      char c = value.charAt(ii++) ;
      switch (state)
      {
        case SCANNING_STATE:
        {
          if (c == '\\')
          {
            state = FOUND1_STATE ;
          }
          else
          {
            temp.append(c) ;
          }
        }
        break ;

        case FOUND1_STATE:
        {
          if (c == '\\')
          {
            temp.append(c) ;
            state = SCANNING_STATE ;
          }
          else
          {
            expr.append(c) ;
            state = EXPR_STATE ;
          }
        }
        break ;

        case EXPR_STATE:
        {
          if (c == '\\')
          {
            state = FOUND2_STATE ;
          }
          else
          {
            expr.append(c) ;
          }
        }
        break ;

        case FOUND2_STATE:
        {
          if (c =  = '\\')
          {
            state = EXPR_STATE ;
            expr.append(c) ;
          }
          else
          {
             processParam(temp, expr) ;
             state = SCANNING_STATE ;
             temp = new StringBuffer("") ; temp.append(c) ;
             expr = new StringBuffer("") ;
          }
        }
        break ;
      }
    }
    if (state == FOUND1_STATE || state == EXPR_STATE)
    {
      compileError("xslx:templtext dangling \\");
    }
    else
    if (state == FOUND2_STATE)
    {
      processParam(temp, expr) ;
    }
    else
    {
      processParam(temp, new StringBuffer("")) ;
    }
  }

  private void processParam(StringBuffer prefix, StringBuffer expr)
  {
    if (expr.length( ) == 0)
    {
      m_TemplParms.addElement(new CkBkTemplParam(new String(prefix))) ;
    }
    else
    {
      processParamExpr(prefix, expr) ;
    }
  }

  private void processParamExpr(StringBuffer prefix, StringBuffer expr)
  {
    int ii = 0 ;
    int len = expr.length( ) ;

    int state = SCANNING_STATE ;
    StringBuffer list = new StringBuffer("") ;
    StringBuffer delimit = new StringBuffer("") ;
    while(ii < len)
    {
      char c = expr.charAt(ii++) ;
      switch (state)
      {
        case SCANNING_STATE:
        {
          if (c == '%')
          {
            state = FOUND1_STATE ;
          }
          else
          {
            list.append(c) ;
          }
        }
        break ;

        case FOUND1_STATE:
        {
          if (c == '%')
          {
            list.append(c) ;
            state = SCANNING_STATE ;
          }
          else
          {
            delimit.append(c) ;
            state = DELIMIT_STATE ;
          }
        }
        break ;

          case DELIMIT_STATE:
          {
            if (c == '%')
            {
              state = FOUND2_STATE ;
            }
            else
            {
              delimit.append(c) ;
            }
          }
          break ;
        }
      }
      try
      {
      if (state =  = FOUND1_STATE)
      {
        compileError("xslx:templtext trailing %");
      }
      else
      if (state == FOUND2_STATE)
      {
        compileError("xslx:templtext extra %");
      }
      else
      if (state =  = SCANNING_STATE)
      {
        String prefixStr = new String(prefix) ;
        Expression value = makeExpression(new String(list)) ;
        m_TemplParms.addElement(new CkBkValueTemplParam(prefixStr, value)) ;
      }
      else
      {
        String prefixStr = new String(prefix) ;
        Expression listExpr = makeExpression(new String(list)) ;
        Expression delimitExpr = makeExpression(new String(delimit)) ;
        m_TemplParms.addElement(new CkBkListTemplParam(prefixStr, listExpr, delimitExpr)) ;
      }
    }
    catch(Exception e)
    {
    }
  }
  // Ein Vektor von CBkTemplParms parse form text
  private Vector m_TemplParms = null;
}

Die Funktionalität von xslx:templtext können Sie um einige nützliche Verbesserungen erweitern. Sie könnten z.B. die Funktionalität der Listen-Escape-Sequenz auf mehrere Listen erweitern (z.B. /expr1%delim1%expr2%delim2/.). Das XSLT-Äquivalent dieser Verbesserung sähe ungefähr wie folgt aus:

<xsl:for-each select="expr1">
  <xsl:variable name="pos" select="position( )"/>
  <xsl:value-of select="."/>
  <xsl:if test="$pos != last( )">
    <xsl:value-of select="delim1"/>
  </xsl:if>
  <xsl:value-of select="expr2[$pos]"/>
  <xsl:if test="$pos != last( )">
    <xsl:value-of select="delim2"/>
  </xsl:if>
</xsl:for-each >

Dieses Hilfsmttel wäre dann nützlich, wenn Sie Listenpaare in Text auflösen müssen. Betrachten Sie z.B. die Parameter einer C++-Funktion, die aus Paaren mit jeweils einem Namen und einem Typ bestehen. Der XSLT-Code ist nur eine recht grobe Spezifikation der Semantik, da er davon ausgeht, dass die in expr1 und expr2 angegebenen Knotenmengen die gleiche Anzahl von Elementen haben. Ich denke, eine echte Implementierung würde die Listen so lange weiterexpandieren, solange es Knoten in einer der Mengen gibt, und Trennzeichen bei jenen unterdrücken, die keine Knoten mehr haben. Noch besser wäre es, das Verhalten mit den Attributen von xslx:templtext zu steuern.

Diskussion

An dieser Stelle erlaubt der begrenzte Platz leider nicht, vollständige Implementierungen dieser Erweiterungselemente in Xalan anzugeben. Aber mit Hilfe der Informationen in der Einleitung sollte der Weg dorthin relativ klar sein.

  

zum Seitenanfang

<< zurück vor >>

 

 

 

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

Copyright © 2006 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 "XSLT Kochbuch" 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