Der XML-Switch

(Auszug aus "Python & XML" von Christopher A. Jones & Fred L. Drake, Jr.)

Der XML-Switch ist das Herzstück des verteilten Systems. Er ist der große Vermittler zwischen den Verbrauchern und Produzenten von Informationen. Beim XML-Switch dreht es sich vor allem um folgende zwei Konzepte:

  1. Er ist dafür gedacht, als Vermittler zwischen Frontend-Anwendungssystemen und Backend-Informationssystemen zu agieren.
  2. Er basiert auf einer fundamentalen XML-Nachrichtenstruktur, um eine höhere Flexibilität zwischen dem Nachrichtensender und -Empfänger zu erreichen.

Die XML-Architektur

Die Grundarchitektur des XML-Switch ist Nachrichten- und RPC-basiert. Der Switch ist ein Vermittler zwischen Frontend-Systemanwendungen wie Webservern oder Desktop-Anwendungen und Backend-Systemen wie Datenbanken und entfernten Diensten. Durch die Verwendung eines Nachrichtenparadigmas anstelle einer direkten Verdrahtung der Systeme miteinander bekommen Sie ein Verkehrsmuster, das von den Anwendungen losgelöst ist und unabhängig davon verwaltet werden kann.

Wenn Sie ein CGI-Skript flicken wollen, das direkt auf eine Datenbank zugreift, müßten Sie ein spezielles Objekt benutzen, das sich nicht nur mit der Datenbank verbindet, sondern auch deren Schema und Datentypen versteht. Andererseits müssen sich Ihre bzw. andere Anwendungen durch den Übergang zu XML nur mit einer XML-Datenstruktur vertraut machen. Diese Datenstruktur wird von der Datenbank produziert, wenn sie mit der richtigen XML-Nachricht danach gefragt wird. Der wesentliche Unterschied hierbei ist, daß diese Art Nachrichten von jedem beliebigen System interpretiert werden können, das sie verstehen muß, sei es heute oder erst in vielen Jahren. Diese Art von Flexibilität macht sich beim Entwurf von verteilten Systemen sehr bezahlt, die sich über lange Zeit weiterentwickeln müssen.

Der hier vorgestellte XML-Switch ist ein einfacher Nachrichtenprototyp, der die Verwendung von XML-Nachrichten in verteilten Python-Systemen erleichtert. Idealerweise sollte Ihr Nachrichtenformat SOAP oder irgendein anderes Format sein, das leicht zwischen den aufkommenden kommerziellen Systemen ausgetauscht werden kann. Die Beschränkungen dieses Buches erlauben keine vollständige Entwicklung eines SOAP-Nachrichtenservers samt Client-Beispielanwendungen. Daher wurde ein einfaches XML-Nachrichtenformat ausgewählt, das die gleiche Art von RPC- und Nachrichtenfunktionalität unterstützt.

Kernklassen von XML-Switch

Der XML-Switch besteht aus drei Hauptteilen. Zuerst gibt es die XMLMessage. Diese Klasse ist die Basiseinheit des Systems. Zusammen mit ihrer assoziierten XML-Nachrichtenstruktur wird diese Klasse als Grundlage der Kommunikation zwischen dem XML-Switch und seinen benachbarten Anwendungen benutzt. Jeder Client, auf jeder Plattform, kann die richtige Art von XML-Nachricht erzeugen, die vom Switch verstanden wird. Das Nachrichtenformat ist ausschlaggebend dafür, daß das System funktioniert.

Ebenso wichtig im Triumvirat der beteiligten Akteure ist der eigentliche XMLSwitch-Handler. Diese Klasse implementiert den HTTP-Handler, der Aufrufe an den Server abfängt. Die Aufgabe vom XMLSwitchHandler ist es sicherzustellen, daß RPC-Nachrichten korrekt geparst und ausgeführt werden und daß deren Rückgabeergebnisse schnell in XML an den Aufrufer zurückgesendet werden.

Der gesamte Nachrichtenaustausch zwischen dem Switch und den Backend-Systemen, mit denen er (über Objekte) verbunden ist, wird von Clients initiiert. Die Clients von XML-Switch benutzen die Klasse xsc, um dem Switch XML-Nachrichten zu senden. Aufgrund ihres Black-Box-Entwurfs verschwinden die Nachrichten im Switch, und die Informationen kommen im XML-Format zurück!

XMLMessage
Diese Klasse wird in XMLMessage.py definiert; siehe Beispiel XMLMessage.py. Sie kapselt das Standardnachrichtenformat der Anwendung von den Entwicklern ab. Eine Beispielnachricht (message.xml) wird im Beispiel Ein Beispiel für die Datei message.xml gezeigt.

XMLSwitchHandler
Diese Klasse wird in XMLSwitchHandler.py definiert; siehe Beispiel XMLSwitchHandler.py später in diesem Abschnitt. Sie startet den XML-Switching-Server, der XML-Nachrichten von den Endbenutzer-Anwendungen annimmt und sie mit den Backend-Ressourcen verbindet. Die von diesen Ressourcen gelieferten Ergebnisse werden in einer weiteren XML-Nachricht an die Ausgangsanwendung zurückgegeben.

xsc
Diese Klasse bietet eine Client-API mit einer Methode, um Nachrichten an den XML-Switch zu senden. Die Methode sendMessage erwartet einen wohlgeformten XML-Nachrichten-String als Argument und sendet das XML zum Switch. Wenn alles gutgeht, stößt der Server die Methode mit den Parametern auf einem der möglichen Objekte an und liefert Ihnen das Ergebnis zurück.

Die XMLMessage-Klasse

In diesem verteilten System werden Nachrichten zwischen den Systemen in einem einfachen XML-Umschlag versendet. Dieser Umschlag (Envelope) ist in seiner Struktur ähnlich zu SOAP. Aber wegen der noch im Entstehen begriffenen SOAP-Unterstützung in Python und dem begrenzten Platz in diesem Buch verwendet das verteilte System in diesem Abschnitt folgende einfache Nachrichtenstruktur (in leerer Form):

<message>
  <header></header>
  <body></body>
</message>

Solange das Dokument auf diese Art organisiert ist, können die Elemente enthalten, was immer Sie wünschen, inklusive SOAP-Fragmente, Webseiten, Datensätze oder was immer Sie innerhalb von XML-Tags plazieren können.

Das XMLMessage-Format

Das folgende Beispiel zeigt eine komplette, wohlgeformte XML-Nachricht:

Beispiel: Ein Beispiel für die Datei message.xml

<message>
  <header><rpc/></header>
  <body>
    <object class="CustomerProfile" method="getProfile">
      <param>234-E838839</param>
    </object>
  </body>
</message>

Das Nachrichtenformat ist nur eine dünne Hülle, bestehend aus den Elementen message, header und body. Die Nachricht im obigen Beispiel ist ein RPC-Aufruf. Wenn der Server das obige Beispiel empfängt, untersucht er zuerst den Header, um festzustellen, daß es sich um einen RPC-Aufruf handelt. Als nächstes extrahiert er die Nutzlast und stößt auf dem korrekten Objekt die richtige Methode mit den entsprechenden Parametern an. Dann ändert er die XML-Nachricht und sendet sie durch den XML-Switch zum Aufrufer zurück.

Die XMLMessage-Klasse

Die XMLMessage-Klasse ist einfach zu verwenden. Nachrichten können entweder aus einem XML-String oder einem XML-Dokumentobjekt erzeugt oder aus einer Datei geladen werden. Danach machen Zugriffsfunktionen es möglich, an bestimmte Teile des Nachrichtendokuments schneller heranzukommen. Mit den Methoden getHeader und getBody können Sie schnell Header- oder Body-Daten extrahieren. Die Methoden setHeader und setBody ermöglichen es Ihnen, eine XML-Nachricht zu bearbeiten, bevor sie zur Weiterverarbeitung an ein anderes System gesendet wird. Die ganze Nachricht kann zwischen einer Darstellung als String oder DOM-Objekt mit den Methoden getXMLMessage und setXMLMessage sowie den DOM-Entsprechungen getXMLMessageDom und setXMLMessageDom ausgetauscht werden. Die üblicherweise benutzten Methoden zum Laden und Inspizieren einer XML-Nachricht (z. B. message.xml im letzten Beispiel) werden in dem kurzen Skript im nächsten Beispiel dargestellt.

Beispiel: runxm.py – Verwendung eines XMLMessage-Objekts

"""
runxm.py - führe XMLMessage-Objekt aus
"""
import XMLMessage
from xml.dom.ext import PrettyPrint
#from xml.dom.ext.reader.Sax2 import FromXml

xm = XMLMessage.XMLMessage( )

xm.loadXMLMessage("message.xml")

from xml.dom.ext import PrettyPrint
PrettyPrint(xm.getXMLMessageDom( ))

print "Ändern des Body zu: <body>Hallo!</body>"
if xm.setBody("<body>Hallo!</body>"):
print xm.getXMLMessage( )

Dieser Code produziert folgende Ausgabe:

G:\pythonxml\c10>python runxm.py
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE message>
<message>
  <header>
    <rpc/>
  </header>
  <body>
    <!-- cp.getProfile("234-E838839") -->
    <object method='getProfile' class='CustomerProfile'>
      <param>234-E838839</param>
    </object>
  </body>
</message>
Ändern des Body zu: <body>Hallo!</body>
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE message>
<message>
  <header>
    <rpc/>
  </header>
  <body>Hallo!</body>
</message>

Diese Ausgabe zeigt, daß die ursprüngliche XML-Nachricht erfolgreich geladen und ihr Body-Element erfolgreich modifiziert wurde. Die Methoden der XMLMessage-Klasse sind simpel, und die meisten verhalten sich gleich. Hier ist eine kurze Referenz der von der XMLMessage-Klasse implementierten Methoden:

setBody(strXML)
setBody nimmt einen XML-String, der ein wohlgeformtes body-Element darstellt, und ersetzt das body-Element der Nachricht durch den neuen Inhalt.

getBody( )
Gibt das body-Element als XML-String zurück (gespeichert in self._body ).

setHeader(strXML)
Ersetzt das vorhandene header-Element durch den angegebenen XML-String.

getHeader( )
Gibt das header-Element als String zurück (gespeichert in self._header).

setXMLMessage(strXMLMessage)
Erwartet ein XML-Nachrichtendokument als String. Der angegebene Parameter wird dann als gesamte XML-Nachricht benutzt. Der neue Inhalt wird in allen weiteren Aufrufen von getBody, getHeader und getXMLMessage zurückgegeben.

setXMLMessageDom(xmldom)
Identisch mit setXMLMessage, nimmt jedoch ein XML-DOM-Objekt an, das eine wohlgeformte Nachricht statt eines XML-Strings darstellt.

loadXMLMessage(file)
Setzt den Inhalt der aktuellen XML-Nachricht auf den Inhalt von file, vorausgesetzt, dieser ist wohlgeformt.

getXMLMessage( )
Gibt das gesamte XML-Nachrichtendokument als String zurück.

getXMLMessageDom( )
Gibt das gesamte XML-Nachrichtendokument als DOM-Instanz zurück.

Zur Implementierung dieser Methoden wurde ein großer Teil der bisher in diesem Buch erarbeiteten DOM-Kenntnisse verwendet. Allerdings gibt es auch einige wenige neue, bemerkenswerte Techniken, die im nächsten Abschnitt erwähnt werden.

Die Code-Architektur von XML-Nachrichten

Das meiste der Arbeit, die in der XMLMessage-Klasse verrichtet wird, geschieht in der setXMLMessage-Methode. Diese Methode nimmt einen versteckten DOM-Parameter, der angibt, ob die neue Nachricht ein XML-String oder eine DOM-Instanz ist.

Das komplette Dokument wird erzeugt, und dann werden die Elemente eingesetzt, indem die entsprechenden Elementnamen aus dem Dokument extrahiert werden. Das gestattet der XMLMessage-Klasse, Zugriffsmethoden für die beiden meistverwendeten Elemente der Nachricht anzubieten: header und body.

if dom:   
   self._dom = strXMLMessage
   Holder = StringIO.StringIO( )
   PrettyPrint(self._dom, Holder)
   self._xml = Holder.getvalue( )
else:
   dom = FromXml(strXMLMessage)
   self._dom = dom
   self._xml = strXMLMessage

# Header als String
Holder = StringIO.StringIO( )
PrettyPrint(self._dom.getElementsByTagName("header")[0],
         Holder)
self._header = Holder.getvalue( )
# Body als String
Holder = StringIO.StringIO( )
PrettyPrint(self._dom.getElementsByTagName("body")[0],
         Holder)
self._body = Holder.getvalue( )

Dadurch, daß die Elemente am Anfang des Parsens gefüllt werden, werden die Daten, die sie darstellen, als Strings gespeichert und sind sofort für alle Aufrufer zugänglich. Es sollte jedoch erwähnt werden, daß beim Ersetzen eines der Elemente body oder header dieses sozusagen wiederhergestellt und das Dokument erneut als String bearbeitet wird:

def setBody(self, strXML):   
   """
   setBody(strXML) - Das angegebene XML wird
   für den Body der XML-Nachricht benutzt.
   """
   xmlstr = FromXml(str("<message>" +
                     self._header + strXML + "</message>"))
   return self.setXMLMessageDom(xmlstr)

Diese Abkürzung erfordert das erneute Parsen des gesamten Dokuments. Ein anderer Ansatz besteht darin, das Dokument in eine Ansammlung von Knoten zu parsen und jeden mit Zugriffsfunktionen les- und schreibbar zu machen. Dieser DOM-freundliche Ansatz erfordert jedoch wesentlich mehr Code, als hier vorgestellt wird.

Code-Listing von XMLMessage

Das folgende Beispiel zeigt das komplette Listing von XMLMessage.py.

Beispiel: XMLMessage.py

"""
XMLMessage.py - eine Hülle für message.xml-Dokumente
"""
import StringIO
from xml.dom.ext import PrettyPrint
from xml.dom.ext.reader.Sax2 import FromXmlStream, FromXml
class XMLMessage:   
   """
   XMLMessage kapselt ein message.xml-Dokument vor den Benutzern der Klasse.
   """
   def __init__(self):
      self._dom = ""
      self._xml = ""
      
   def setBody(self, strXML):
      """
      setBody(strXML) - Das angegebene XML wird
      für den Body der XML-Nachricht benutzt.
      """
      xmlstr = FromXml(str("<message>" +
      self._header + strXML + "</message>"))
      return self.setXMLMessageDom(xmlstr)
      
   def getBody(self):
      """ Liefert Body als String
      """
      return self._body
      
   def setHeader(self, strXML):
      """
      setHeader(strXML) - Das angegebene XML
      wird als Header der XML-Nachricht benutzt.
      """
      xmlstr = FromXml(str("<message>" + strXML + self._body + "</message>"))
      return self.setXMLMessageDom(xmlstr)

   def getHeader(self):
      """ Liefert Header als String
      """
      return self._header
      
   def setXMLMessage(self, strXMLMessage, dom=0):
      """
      setXMLMessage - benutzt XML als gesamte
      XML-Nachricht
      """
      try:
      if dom:
      # Weise dom direkt mit Parameter zu
      self._dom = strXMLMessage
      
      # Fülle StringIO-Objekt für self._xml
      Holder = StringIO.StringIO()
      PrettyPrint(self._dom, Holder)
      
      # Weise String-Wert von dom an self._xml zu
      self._xml = Holder.getvalue()
      else:
      # Erzeuge dom aus angegebenem XML-String
      dom = FromXml(strXMLMessage)

      # Setze dom als Objektattribut
      self._dom = dom

      # Setze XML-String als Objektattribut
      self._xml = strXMLMessage
      
      # Header als DOM
      self._headerdom = self._dom.getElementsByTagName("header")[0]
      
      # Header als String
      Holder = StringIO.StringIO()
      PrettyPrint(self._dom.getElementsByTagName("header")[0],
      Holder)
      self._header = Holder.getvalue()
      
      # Body als DOM
      self._bodydom = self._dom.getElementsByTagName("body")[0]
      
      # Body als String
      Holder = StringIO.StringIO()
      PrettyPrint(self._dom.getElementsByTagName("body")[0],
      Holder)
      self._body = Holder.getvalue()
      
      except:
      print "DOM konnte nicht aus der Nachricht erzeugt werden!"
      return 0
      
      return 1
      
   def setXMLMessageDom(self, xmldom):
      """ rufe setXMLMessage mit dom-Schalter auf
      """
      return self.setXMLMessage(xmldom, dom=1)
      
   def loadXMLMessage(self, file):
      """
      loadXMLMessage - erstelle eine XML-Nachricht aus einer Datei oder URL
      """
      try:
      dom = FromXmlStream(file)
      except:
      print "XML-Nachricht konnte nicht geladen werden."
      return 0
      
      return self.setXMLMessageDom(dom)
      
   def getXMLMessage(self, dom=0):
      """
      getXMLMessage - liefert gesamte Nachricht entweder als XML-String oder DOM
      """
      if dom:
      return self._dom
      else:
      return self._xml
      
   def getXMLMessageDom(self):
      """ Liefert XML-Nachricht als DOM
      """
      return self.getXMLMessage(dom=1)                  

Die XMLMessage-Klasse kapselt ein einfaches XML-Nachrichtenformat mit Zugriffsmethoden von Entwicklern ab. Ein solcher Ansatz kann verwendet werden, um komplexere Nachrichten als diese, z. B. SOAP, zu umhüllen. Auf diese Art können Sie Komponenten Ihres verteilten Systems bauen, die SOAP sprechen, oder mit der Migration Ihres verteilten Integrationsprojekts nach SOAP und Python beginnen.

Der XML-Switch-Dienst

Der XML-Switch ist ein Serverprozeß und eine Client-API, mit der Objekte ihre Methoden und Eigenschaften über das Web veröffentlichen können. XML-Nachrichten werden ähnlich zu SOAP-Aufrufen verwendet, um Methoden auf Serverobjekten anzustoßen. Da die SOAP-Unterstützung von Python noch recht dünn ist und in diesem Buch nur ein beschränkter Platz zur Verfügung steht, wurde ein einfaches XML-Nachrichtenformat für diese Anwendung entworfen (im vorherigen Abschnitt beschrieben). Diese Nachrichten werden, wenn sie mit einem rpc-Element im Header markiert sind, vom XML-Switch dazu benutzt, Methoden auf einem Objekt aufzurufen und die Ergebnisse in einer weiteren XML-Nachricht zurückzugeben.

Der XML-Switch-Dienst wird größtenteils von der XMLSwitchHandler-Klasse angeboten, die später im Beispiel XMLSwitchHandler.py entwickelt wird. Die in späteren Teilen dieses Abschnitts entwickelten Client-Anwendungen erzeugen XML-Nachrichten und schicken sie an den Server weiter. Der Server inspiziert dann diese XML-Nachrichten, um zu prüfen, ob es RPC-Aufrufe sind. Falls ja, wird das korrekte Objekt geladen, die Methode ausgeführt, und die Rückgabewerte werden in eine andere XML-Nachricht verpackt und zum Aufrufer zurückgesendet.

Es gibt keinen Grund, warum nicht eine Lookup-Tabelle gebaut werden könnte, mit der Routing-Regeln auf die XML-Nachrichten angewendet werden könnten, sobald sie eintreffen. Ebenso gibt es keinerlei Grund dafür, warum ein XML-Switch eine XML-Nachricht nicht zu einem anderen XML-Switch routen könnte – so daß diese Nachricht ihrem Endziel entgegenspringen würde. Das ermöglicht die Entkopplung der Nachrichtenzustellung zwischen Sender und Empfänger. Durch das Verketten von XML-Switches können Sie ein skalierbares, Routing-fähiges XML-Netzwerk aufbauen.

Der XML-Switch-Client

Es gibt zwei Haupt-Clients von XML-Switch. Der erste, postMsg.html, ist einfach nur eine Webseite, die Daten an den korrekten Server und die korrekte URL schickt. Der Switch antwortet mit rohem XML, das der Browser (im Falle von Internet Explorer) in einer Baumansicht darstellt, oder benutzt (etwa bei Netscape) den angegebenen Content-Handler oder zeigt Ihnen die Datei als einfachen Text.

Der XML-Switch-Client ist eine Python-API, die auch als Kommandozeilenwerkzeug benutzt werden kann. Die API bietet eine einzige Methode zum Absenden von XML-Nachrichten an den Server und zum Empfangen von Antworten. In diesem Abschnitt sehen wir uns die Clients von XML-Switch an. Danach bauen wir den Server selbst.

Verwenden von postMsg.html zur Rückgabe von XML

Die Datei postMsg.html ermöglicht es Ihnen, Daten an den Server zu senden und die Methode echoReponse auszuführen, um die Funktionalität Ihres Servers zu testen. Diese Datei wird im folgenden Beispiel gezeigt.

Beispiel: Die Datei postMsg.html

<html>
  <body>
    <form action="http://centauri:2112/" method="POST">
      <p>Eingabe hier:</p>
      <p>
        <textarea name="n" rows=20 cols=80>
          <message>
            <header><non-rpc/></header>
            <body><!-- cp.getProfile("234-E838839") -->
              <object class="CustomerProfile" method="getProfile">
                <param>234-E838839</param>
              </object>
            </body>
          </message>
        </textarea>
      </p>
      <p><input type="submit" value="Daten Abschicken"></p>
    </form>
  </body>
</html>

Die Methode echoResponse eignet sich sehr gut dazu, die Funktionalität des Servers zu testen. Wenn Sie die im obigen Beispiel erzeugte Datei postMsg.html verwenden, können Sie eine Beispielnachricht senden und bekommen eine Antwort vom XML-Switch zurück, wie in der folgenden Abbildung gezeigt.

Verwendung von postMsg.html für die Verbindung zum Server

Abbildung: Verwendung von postMsg.html für die Verbindung zum Server

Wenn Sie eine Nachricht mit einem Header erstellen, in dem <rpc/> statt <non-rpc/> steht, bekommen Sie jedoch die XML-Antwort, die die Nachricht erzeugt, wenn der RPC vom Server ausgeführt wird.

Wenn Sie z. B. folgendes XML in postMsg.html eingeben

<message>
  <header><rpc/></header>
  <body>
    <object class="CustomerProfile" method="getProfile">
      <param>983-E2229J3</param>
    </object>
  </body>
</message>

und auf submit data klicken, bekommen Sie das rohe XML-Paket vom Server zurück. Mit Internet Explorer als Browser wird es mit dem Standard-Stylesheet angezeigt, wie in der folgenden Abbildung zu sehen. Das funktioniert nur, wenn Sie ein Profil mit der ID 983-E2229J3 in Ihrer Datenbank haben. Wenn nicht, ersetzen Sie einfach diesen ID-Wert durch einen, der in Ihrer Datenbank existiert, und dann sollte es funktionieren.

Absetzen eines RPC-Aufrufs mit postMsg.html

Abbildung: Absetzen eines RPC-Aufrufs mit postMsg.html

Tatsächlich sollte postMsg.html für jede gültige XML-Nachricht funktionieren, die zum Server geschickt wurde. Um direkt zu beweisen, daß die API funktioniert, müssen Sie den xsc-Client aus der Kommandozeile oder aus Python-Code starten.

Verwenden des XSC-Clients

Mit dem in Beispiel xsc.py, der XML-Switch-Client abgebildeten xsc-Client xsc.py können Sie Aufrufe im XML-Switch durchführen und die XML-Nachrichten inspizieren, die dabei zurückgeschickt werden. Die XML-Nachrichten müssen in einer lokalen Datei aufbewahrt werden, wenn xsc als Kommandozeilenwerkzeug benutzt wird.

Die XML-Datei ist ein einfaches Nachrichtendokument. Das folgende Beispiel zeigt eine Beispielnachricht, msgGetProfile.xml, die Sie mit xsc benutzen können.

Beispiel: msgGetProfile.xml

<message>
  <header><rpc/></header>
  <body>
    <object class="CustomerProfile" method="getProfile">
      <param>983-E2229J3</param>
    </object>
  </body>
</message>

Starten Sie die Datei mit der Nachrichtendatei als Parameter, wie im folgenden Beispiel gezeigt.

Beispiel: Starten von xcs.py von der Kommandozeile aus

G:\pythonxml\c10> python xsc.py msgGetProfile.xml
XMLSwitch-Server: localhost:2112
[200 OK 522 bytes]
Antwort:

<message>
  <header>
    <rpc-response/>
  </header>
  <body>
    <object method='getProfile' class='CustomerProfile'>
      <response>
        <CustomerProfile id='983-E2229J3'>
          <firstname>Larry</firstname>
          <lastname>BoBerry</lastname>
          <address1>Northpoint Apartments</address1>
          <address2>Apt. 2087</address2>
          <city>Lemmonville</city>
          <state>MD</state>
          <zip>12345</zip>
        </CustomerProfile>
      </response>
    </object>
  </body>
</message>

Die xsc-Kommandozeilen-Operation gibt eine Statuszeile aus, die den benutzten Server angibt, eine Zeile mit HTTP-Antwortcode und -Nachricht und die Größe des gelieferten XML-Dokuments. Das zurückgegebene XML wird dann auf der Kommandozeile ausgegeben.

Verwenden der XSC-API

Sie können auch Aufrufe von XML-Switch aus Ihren eigenen Programmen vornehmen. Tatsächlich kommunizieren die später in diesem Abschnitt vorgestellten Client-Anwendungen mit anderen Systemen über den xsc-Client und kleine XML-rpc-Nachrichten.

Um die xsc-API zu benutzen, müssen Sie xsc in Ihre Klasse importieren.

import sys
import xsc

xc = xsc.xsc( )

Als nächstes müssen Sie das Server/Port-Paar angeben, wo der XML-Switch läuft:

 xc.server = "localhost:2112" 

Sie brauchen auch etwas XML-Code, den Sie dem Server senden. Es kann nicht schaden, die Nachricht aus einer Datei zu laden.

fd = open(sys.argv[1], "r")
xmlmsg = fd.read( )
fd.close( )

Schließlich reicht ein Methodenaufruf, um Ihre XML-Nachricht an den Server zu schicken und ein Resultat zu bekommen:

response = xc.sendMessage(xmlmsg)
print response

Das ist alles, was man braucht, um entfernte Python-Objekte aufzurufen, die in SQL-Datenbanken schauen und XML-Datenlager inspizieren, um relevante Informationen zu finden. Der XML-Switch agiert als Vermittler, nimmt Ihre XML-Anfragen auf und sendet Ihnen XML-Informationen zurück.

Der komplette Code von xsc.py, der Datei, die sowohl für Kommandozeilen-Anfragen an den XML-Switch wie auch für dessen programmatische Benutzung benötigt wird, ist im folgenden Beispiel wiedergegeben.

Beispiel: xsc.py, der XML-Switch-Client

"""
xsc.py - XMLSwitch-Client
Benutzung: python xsc.py meineAnfrageDatei.xml

"""
import sys
import httplib
from urllib import quote_plus

class xsc:      
   """
      xsc - XMLSwitch-Client
      Diese Klasse ist sowohl die Kommandozeilen- als auch
      die Modulschnittstelle zu XMLSwitch.
      
      Von der Kommandozeile:
      $> python xsc.py msgDatei.xml
      
      Der dritte Parameter ist eine XML-Datei mit einer gültigen <message> darin.
      Die Antwort-<message> wird auf die Konsole geschrieben werden.
      
      Als API:
      import xsc
      responseXML = xsc.sendMessage(strXMLMessage)
      
      Das Ergebnis ist nun in responseXML.
      """
      
    def __init__(self):
      """
         init - setze einige öffentliche Attribute
      """
      self.server = "localhost:2112" # host:port (80 ist http)
      self.stats = ""

    def sendMessage(self, strXMLMessage):
      """
      sendMessage(strXML) - diese Methode sendet die
      angegebene XML-Nachricht an den Server in self.server.
      Die XML-Antwort wird an den Aufrufer zurückgegeben.
      """
      # Bereite XML-Nachricht durch Codieren der URL vor...
      strXMLRequest = quote_plus(strXMLMessage)
      
      # Verbinde mit Server...
      req = httplib.HTTP(self.server)
      
      # Füge HTTP-Header hinzu, inkl. content-length
      # als Größe unserer XML-Nachricht
      req.putrequest("POST", "/")
      req.putheader("Accept", "text/html")
      req.putheader("Accept", "text/xml")
      req.putheader("User-Agent", "xsc.py")
      req.putheader("Content-length", str(len("n=" + strXMLRequest)))
      req.endheaders()
      
      # Sende XML als POST-Daten
      req.send("n=" + strXMLRequest)
      
      # Empfange HTTP-Antwort
      ec, em, h = req.getreply()
      
      # content-length gibt Anzahl von Bytes in der XML-Antwortnachricht an
      cl = h.get("content-length", "0")
      
      # Status [http-code, http-msg, content-length]
      self.stats = ("[" + str(ec) + " " +
      str(em) + " " +
      str(cl) + " bytes]")
      
     # Versuche XML-Antwort zu lesen
     nfd = req.getfile()
     try:
      textlines = nfd.read()
      nfd.close()
      # Gib XML-Daten zurück
      return textlines
      
     except:
      # Beende nach Ausnahme
      nfd.close()
      return ""
      
    # Kommandozeilen-Betrieb
    if __name__ == "__main__":
      # Instanziiere Server
      xc = xsc()
      xc.server = "localhost:2112"
      
      # Lies Nachrichtendatei
      fd = open(sys.argv[1], "r")
      xmlmsg = fd.read()
      fd.close()
      
      # Rufe Server auf und gib Status und Antwort aus
      print "XMLSwitch-Server: ", xc.server
      response = xc.sendMessage(xmlmsg)
      print xc.stats
      print "Antwort: "
      print response

Die Server-Klasse XMLSwitchHandler

Die Klasse XMLSwitchHandler ist ein BaseHTTPRequestHandler. Die vollständige XMLSwitch-Handler-Klasse ist in Beispiel XMLSwitchHandler.py abgebildet. Sie können das zusätzliche Skript runxs.py dazu benutzen, den Server tatsächlich von der Kommandozeile zu starten. Dieses Skript wird in Beispiel Das Startskript für den XML-Switch: runxs.py gezeigt.

Die Code-Architektur von XMLSwitchHandler

Die Architektur hinter dem XMLSwitchHandler umfaßt eine große Menge von XML. Wahrscheinlich ist es am besten, die Methode processXMLMessagePost hervorzuheben. Sie ist das wirkliche Arbeitspferd. Die Nachrichten kommen als URL-codierte Daten herein. Um die vom Client gesendete Nachricht zu verstehen, ist es notwendig, die Daten zu decodieren und zu versuchen, ein DOM-freundliches XMLMessage-Objekt aus dem Resultat zu erhalten:

def processXMLMessagePost(self, strPostData):    
    """
    processXMLMessagePost(strXMLMessage) - diese Methode erzeugt eine
    XMLMessage aus den angegebenen Daten und sucht nach einer Abbildung
    in XMLMapping.xml, um zu bestimmen, welches Objekt/Methode-Paar
    aufzurufen ist.
    """
    
    # Erzeuge Nachricht durch Decodieren der post-Daten
    xmsg = XMLMessage( )
    xmsg.setXMLMessage(
      unquote_plus(strPostData).replace("n=", ""))

An diesem Punkt ist xmsg ein neues XMLMessage-Objekt, das die Anfrage des Clients kapselt. Jetzt wird der header inspiziert. Falls sein Textinhalt <rpc/> ist, weiß der Server, daß er die Nachricht als rpc-Aufruf verarbeiten soll. Sonst wird sie als non-rpc betrachtet und an die Methode echoResponse geschickt, die HTML benutzt, um die Anfrage an den Client zurückzuschicken.

# Teste Header auf Element <rpc/>
strHeader = xmsg.getHeader( )
if strHeader.rfind("<rpc/>") < 0:
   # Sende ein HTML-Echo zurück
   self.echoResponse(strPostData)
   return 0

Ist die Nachricht tatsächlich ein RPC-Kandidat, so ist es wichtig, den Objekt- und Methodennamen zu extrahieren, ebenso wie die angegebenen Parameter. Das ist nicht ganz einfach, wie folgender Code demonstriert:

# Werte object.method(params) aus
msgDom = xmsg.getXMLMessageDom( )
objElem = msgDom.getElementsByTagName("object")[0]
object = objElem.getAttributeNS('',"class")
method = objElem.getAttributeNS('',"method")
params = []
paramElems = msgDom.getElementsByTagName("param")   

# Hole Parameter als Strings
for thisparam in paramElems:
   strParam = StringIO.StringIO( )
   PrettyPrint(thisparam, strParam)
   parameter = strParam.getvalue().strip( )
   parameter = parameter.replace("<param>", "")
   parameter = parameter.replace("</param>", "")
   params.append(parameter)

Nach der Extraktion der für das Kommando notwendigen Daten können Sie anfangen, den Kommando-String vorzubereiten. Er enthält den Namen der lokalen Objektinstanz, den Namen der aufzurufenden Methode und die angegebenen Parameter. Das Kommando wird entsprechend vorbereitet:

# Instanziiere korrektes Objekt
if object == "CustomerProfile":
    from CustomerProfile import CustomerProfile
    inst = CustomerProfile( )    

if object == "XMLOffer":
    from XMLOffer import XMLOffer
    inst = XMLOffer( )

# '''''''''''''''''''''''''''''''''''''''''''''''''''''
# Fügen Sie hier weitere Objekt-Instanziierungen hinzu
# '''''''''''''''''''''''''''''''''''''''''''''''''''''

# Bereite cmd-String vor
cmd = "inst." + method + "("

# Füge Parameter an Kommando hinzu, wenn nötig,
# getrennt durch """ und Kommas
if len(params) == 1:
    cmd += '"""' + params[0] + '""")'
elif len(params) > 1:
for pmIndex in range(len(params) - 1):
        cmd += '"""' + params[pmIndex] + '""", '
    cmd += '"""' + params[len(params)-1] + '""")'

# Ohne Parameter schließe einfach die Klammern: ( )
if not params:
        cmd += ")"

Der vorige Code zeigt den vorsichtigen Umgang mit DOM zur Extraktion der notwendigen Kommandowerte aus der XML-Nachricht. Diese verschiedenen Werte werden dann in einem einzigen cmd-String kombiniert. Die hervorgehobenen Codezeilen zeigen, wo der Wert von cmd verändert wird, um das Kommando zu vervollständigen. Das Kommando für die vorhergehenden msgGetProfile.xml-Aufrufe hätte vom XMLSwitchHandler übersetzt werden müssen, um wie folgt auszusehen:

 inst.getProfile("983-E2229J3") 

Die dreifachen Anführungszeichen werden benutzt, um vorhandene einfache oder doppelte Anführungszeichen zu schützen, die eventuell in den Parametern enthalten sind. Wenn jedoch dreifache Anführungszeichen im Argument benutzt werden, bricht der Vorgang natürlich zusammen!

Nachdem die Methoden vorbereitet wurden, können wir die Python-Anweisung eval benutzen, um die Objekte anzustoßen und die passenden Methoden aufzurufen:

 result = eval(cmd) 

Nach dem Methodenaufruf wird das Ergebnis dann dazu benutzt, eine XML-Antwortnachricht zu erstellen. Dies geschieht mit einem temporären DOM mit den neuen Werten, der dann in serialisierter Form auf die Socket-Verbindung zum Client geschrieben wird. Wenn sie einmal in DOM-Form ist, kann sie natürlich validiert werden, wenn Sie möchten, oder verändert werden, um einen Dokumentprolog oder andere Arten von Information zu entfernen, die beim Einbetten in ein anderes XML-Dokument stören.

Das XMLSwitchHandler-Skript

Das folgende Beispiel zeigt das vollständige Listing von XMLSwitchHandler.py:

Beispiel: XMLSwitchHandler.py

"""
XMLSwitchHandler.py
"""
import sys
import BaseHTTPServer
import StringIO

from urllib import unquote_plus      
from XMLMessage import XMLMessage
from xml.dom.ext import PrettyPrint
from xml.dom.ext.reader.Sax2 import FromXml

class XMLSwitchHandler(
      BaseHTTPServer.BaseHTTPRequestHandler):
   def do_GET(self):
      """
      do_GET verarbeitet HTTP-GET-Anfragen auf dem Server-Port.
      """
      
      # Sende generische HTML-Antwort
      self.send_response(200)
      self.send_header("Content-type", "text/html")
      self.end_headers()
      self.wfile.write("<html><body>")
      self.wfile.write("<font face=tahoma size=2>")
      self.wfile.write("<b>Hallo vom XMLSwitchHandler!</b>")
      self.wfile.write("</font></body></html>")
      
    def do_POST(self):
      """
      do_POST verarbeitet HTTP-POST-Anfragen und XML-Pakete.
      """
      if self.headers.dict.has_key("content-length"):
      # Konvertiere content-length von String nach Ganzzahl
      content_length = int(self.headers.dict["content-length"])

      # Lies korrekte Anzahl von Bytes vom Client
      # und verarbeite die Daten
      raw_post_data = self.rfile.read(content_length)
      self.processXMLMessagePost(raw_post_data)
      return 1

      else:
      # fehlerhafte post-Operation
      self.send_reponse(500)
      return 0
      def processXMLMessagePost(self, strPostData):
      """
      processXMLMessagePost(strXMLMessage) - diese Methode erzeugt eine
      XMLMessage aus den angegebenen Daten und sucht nach einer Abbildung
      in XMLMapping.xml, um zu bestimmen, welches Objekt/Methode-Paar
      aufzurufen ist.
      """

      # Erzeuge Nachricht durch Decodieren der post-Daten
      xmsg = XMLMessage()
      xmsg.setXMLMessage(
      unquote_plus(strPostData).replace("n=", ""))
      
      # Teste Header auf Element <rpc/>
      strHeader = xmsg.getHeader()
      if strHeader.rfind("<rpc/>") < 0:
      # Sende ein HTML-Echo zurück
      self.echoResponse(strPostData)
      return 0
      
      # Werte object.method(params) aus
      msgDom = xmsg.getXMLMessageDom()
      objElem = msgDom.getElementsByTagName("object")[0]
      object = objElem.getAttributeNS('',"class")
      method = objElem.getAttributeNS('',"method")
      params = []
      paramElems = msgDom.getElementsByTagName("param")
      
      # Hole Parameter als Strings
      for thisparam in paramElems:
      strParam = StringIO.StringIO()
      PrettyPrint(thisparam, strParam)
      parameter = strParam.getvalue().strip()
      parameter = parameter.replace("<param>", "")
      parameter = parameter.replace("</param>", "")
      params.append(parameter)
      
      # Instanziiere korrektes Objekt
      if object == "CustomerProfile":
      from CustomerProfile import CustomerProfile
      inst = CustomerProfile()
      
      if object == "XMLOffer":
      from XMLOffer import XMLOffer
      inst = XMLOffer()
      
      # '''''''''''''''''''''''''''''''''''''''''''''''''''''
      # Fügen Sie hier weitere Objekt-Instanziierungen hinzu
      # '''''''''''''''''''''''''''''''''''''''''''''''''''''
      
      # Bereite cmd-String vor
      cmd = "inst." + method + "("
      
      # Füge Parameter an Kommando hinzu, wenn nötig,
      # getrennt durch """ und Kommas
      if len(params) == 1:
      cmd += "\"\"\"" + params[0] + "\"\"\"" + ")"
      elif(len(params) > 1):
      for pmIndex in range(0, (len(params) - 1)):
      cmd += "\"\"\"" + params[pmIndex] + "\"\"\"" + ", "
      cmd += "\"\"\"" + params[len(params)-1] + "\"\"\")"
      
      # Ohne Parameter schließe einfach die Klammern: ()
      if not params:
      cmd += ")"
      
      # Führe Kommando aus und halte Ergebnis fest
      rezult = eval(cmd)
      
      # Baue XML-Antwort
      returnDom = FromXml(
      "<message>\n\t<header>\n\t\t<rpc-response/>\n\t</header>\n" +
      "\t<body>\n\t\t<object class=\"" + str(object) + "\" method=\"" +
      str(method) + "\">\n\n\t\t\t<response>" + str(rezult) +
      "</response>\n\t\t</object>\n\t</body>\n</message>\n")
      
      # optional: validiere gegen Ergebnis-DOM oder
      # irgendeine andere spezielle Logik
      
      # Bereite String des Dokumentelements vor
      # (schneide Prolog für die XML-Nachricht heraus)
      strReturnXml = StringIO.StringIO()
      PrettyPrint(returnDom.documentElement, strReturnXml)
      xmlval = strReturnXml.getvalue()
      
      # Gib XML über HTTP an Aufrufer zurück
      self.send_response(200)
      self.send_header("Content-type", "text/xml")
      self.send_header("Content-length", str(len(xmlval)))
      self.end_headers()
      self.wfile.write(str(xmlval))
      
      return 1
      
   def echoResponse(self, strPostData):
      """
      echoResponse(postData) - gibt die in Header und Body geparsten post-Daten zurück.
      """
      
      # Sende Antwort
      self.send_response(200)
      self.send_header("Content-type", "text/html")
      self.end_headers()
      
      # Sende HTML-Text
      self.wfile.write("<html><body>"
      "<font color=blue face=tahoma size=5>"
      "<b>Hallo vom XMLSwitchHandler!</b></font><br><br>"
      "<font face=arial,verdana,helvetica size=4>"
      "Versuche, eine XML-Nachricht "
      "zu erstellen...<br><br>Kopf:<br><xmp>")
      
      msg = XMLMessage()
      msg.setXMLMessage(unquote_plus(strPostData).replace("n=", ""))
      
      # Parse Nachricht in Header und Body, zeige als Beispiel auf Webseite an
      self.wfile.write(msg.getHeader())
      self.wfile.write("</xmp></font><font face=arial,verdana,helvetica"
                  " size='4'>Rumpf:<br><xmp>")   
      
      self.wfile.write(msg.getBody())
      self.wfile.write("</xmp></font></body></html>")            
Tipp der data2type-Redaktion:
Zum Thema Python & XML 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 "Python & XML" 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