Alles String, oder was? Datenformate in Mule. Teil 1: XML

3 Kommentare

Einleitung

Gerade für Neulinge können die Datenformate in Mule sehr verwirrend sein: Man bekommt aus einer Queue laut Dokumentation ein XML, klickt daraufhin seinen Flow zusammen, versucht mit den Daten zu arbeiten, stellt aber schnell fest: Es ist gar nicht so einfach. Man bekommt Exceptions, Datentypen stimmen nicht oder MEL-Ausdrücke sind falsch. Dann schmeißt man den Debugger an logged was das Zeug hält. Und sieht? Mal Strings, mal Streams, mal Byte-Arrays.

Als Naivling denke ich: „Klar, XML ist ja eigentlich nur ein Text in einem bestimmten Format“

Als erfahrener Programmierer weiß ich: „XML kann auf verschiedene Arten repräsentiert werden, z.B. als Baumstruktur (DOM). Irgendwie muss das konvertiert werden.“

Und als Bedenkenträger grummel ich vor mich hin: „Strings, Streams, binäre Repräsentationen, Encoding. Lauter Fallstricke“

In Mule müssen XML-Daten nicht unbedingt als Strings vorliegen, sondern können in verschiedenen Format vorkommen – nicht zuletzt der Performance wegen. In diesem Artikel schauen wir uns an, in welchen Formen XML auftreten und wie man damit arbeiten kann. Dabei werden wir viel aus Versuch und Irrtum lernen.

Die XML-Payload

Zunächst weiß man, dass die Payload (genau wie alle Variablen oder Properties) beliebige Java-Objekte enthalten kann und diese von einer Komponente zur nächsten geschoben werden. Sehr stark vereinfacht ändern Transformer die Payload, Connectoren setzen eine neue.

Die Payload ist eine bestimmte Eigenschaft der Message, und im AnypointStudio-Debugger oder mit Hilfe von Logausgaben können wir die Klasse der jeweils aktuellen Payload sehen. Fangen wir an und schreiben einen Flow, der XML-Daten als Body in einem HTTP-Post erwartet. Ich gehe von folgendem Beispiel aus:

<?xml version="1.0" encoding="UTF-8"?>
<person><name>Langmann</name><firstname>Christian</firstname></person>

Ich lege solche Beispiele in der Regel als Dateien unter src/test/resources ab, weil diese auch gerne für MUnit-Tests verwendet werden (mehr dazu in der nächsten Folge oder im Blog über MUnit). Wenn man mit raml arbeitet, bietet sich auch src/main/api/examples an, dann kann man sie auch direkt aus der raml referenzieren.

Der Flow soll daraus einfach eine persönliche Begrüßung erzeugen: „Hallo Christian Langmann“. In der Annahme, dass man auf XML-Elemente wie auf Member zugreifen kann, sieht ein primitiver (fehlerhafter) Versuch erst einmal so aus:

<http:listener-config name="HTTP_Listener_Configuration" 
                      host="0.0.0.0" port="8081" basePath="/xml" 
                      doc:name="HTTP Listener Configuration"/>
<flow name="dataformatsFlow">
   <http:listener config-ref="HTTP_Listener_Configuration" 
                  path="/" allowedMethods="POST" doc:name="HTTP"/>
   <set-payload value="Hallo #[payload.firstname] #[payload.name]"
                doc:name="Set result payload"/>
   <logger level="INFO" doc:name="Logger"/>
</flow>

Schicken wir obiges XML an diesen Flow (z.B. mit Postman), bekommt man „Hallo null null“ zurück. Logisch, denkt man vielleicht: Ich muss ja über die Person navigieren. Allerdings führt die Zeile

<set-payload value="Hallo #[payload.person.firstname] #[payload.person.name]" 
             doc:name="Set result payload"/>

beim Auswerten der MEL in „Set Payload“ zu einer ExpressionRuntimeException.

Starten wir den Debugger, halten auf „Set Payload“ an und betrachten die Payload. Diese ist vom Typ BufferInputStream. Und der hat weder eine „person“, noch „name“ oder „firstname“ als Elemente.

Debugger-BufferInputStream

Sorgen wir zunächst dafür, dass aus dem Stream ein String gemacht wird, um zu gucken, was darin steht (alles Folgende ginge auch mit dem Stream, aber das Verstehen der Transformatoren ist so einfacher): Vor dem Setzen der Payload realisieren wir den Stream, indem wir eine MEL „message.payloadAs(String)“ ausführen. Um das Ganze noch hübscher zu machen, formatieren wir den XML-String durch einen pretty-printer (dieser kann allerdings nicht aus der Oberfläche des AnypointStudios erzeugt werden):

<flow name="dataformatsFlow">
   <http:listener config-ref="HTTP_Listener_Configuration" 
                  path="/" allowedMethods="POST" doc:name="HTTP"/>
   <expression-transformer doc:name="Force stream into mem" 
                           expression="#[message.payloadAs(String)]"/>
   <mulexml:xml-prettyprinter-transformer doc:name="pretty print"/>
   <set-payload value="Hallo #[payload.person.firstname] #[payload.person.name]" 
                doc:name="Set result payload"/>
   <logger level="INFO" doc:name="Logger"/>
</flow>

Achtung: Der Ausdruck „message.payloadAs(…)“ verändert die Payload, auch wenn er beispielsweise in einem Logger verwendet wird! Ich benutze ihn nur zur Fehlersuche.

Nun ist die Payload vom Typ String, und der Inhalt ist wie gewünscht das XML.
Screen Shot 2016-06-29 at 17.22.52
Wenn man genau hinguckt, fällt einem im Debugger das Element „DataType“ auf. Dieser zeigt Details zur aktuellen Payload an. In unserem Fall ist der Value ein SimpleDataType, der neben der Klasse java.lang.String auch Informationen zum Encoding und MIME-Type enthält. Diese Informationen wurden vom Endpoint aus den geschickten Daten gewonnen. Ich habe zum Beispiel im Postman beim Senden des POST-Requests als Datentyp application/xml angegeben. Diese Informationen waren übrigens auch schon für den Stream vorhanden.

Leider schlägt das Setzen des Ergebnisses immer noch fehl. Die MEL kann mit einem XML-String eben genausowenig anfangen, wie mit einem Stream. Tatsächlich müssen wir, um ein XML auszuwerten, auf xpath zurück greifen. Die MEL-Funktion dafür heißt aber xpath3 (um die unterstützte xpath-Version zu benennen). Wir ändern den MEL-Ausdruck in

<set-payload value="Hallo #[xpath3(&quot;/person/firstname&quot;)] #[xpath3(&quot;/person/name&quot;)]" 
             doc:name="Set result payload"/>

und schon bekommen wir das richtige Ergebnis.

Umweg über ein POJO

Angenommen wir haben eine einfache Klasse (ein POJO), die wie folgt aussieht:

package de.codecentric.mule.dataformats;
public class Person {
   private String name;
   private String firstname;
   public String getName() {
      return name;
   }
   public void setName(String name) {
      this.name = name;
   }
   public String getFirstname() {
      return firstname;
   }
   public void setFirstname(String firstname) {
      this.firstname = firstname;
   }
}

Nun können wir aus dem XML dieses Objekt erzeugen und statt der xpath-Ausdrücke direkt auf die Member zugreifen. Der einzige Trick ist, einen Alias für das person-Element für die Klasse zu definieren.

<flow name="dataformatsFlow">
   <http:listener config-ref="HTTP_Listener_Configuration" 
                  path="/" allowedMethods="POST" doc:name="HTTP"/>
   <mulexml:xml-to-object-transformer doc:name="XML to Object">
      <mulexml:alias name="person" class="de.codecentric.mule.dataformats.Person"/>
   </mulexml:xml-to-object-transformer>
   <set-payload value="Hallo #[payload.firstname] #[payload.name]" 
                doc:name="Set result payload"/>
</flow>

Intern verwendet der xml-to-object-transformer dafür XStream. Als Alternative stehen auch Transformer mit JAXB zur Verfügung.

Oder als HashMap

Schön wäre es auch, wenn wir ohne die spezielle (Person-)Klasse auf die Elemente zugreifen könnten. Die quasi Hilfsklasse in Mule für alle Arten von strukturierten Daten ist die HashMap. Nun ist es mit der Universalwaffe für Transformationen in Mule ziemlich einfach, aus einem XML eine HashMap zu erzeugen: Dem DataWeave.

Dabei ist es hilfreich, ein XML-File mit Beispieldaten zu haben (s.o.). Denn dann kann man Metadaten definieren (am besten dort, wo die Daten entstehen, also am Inbound-Endpoint. Das nennt sich dann Metadata Driven Development). In diesem Fall habe ich dort einen Typ xml-Person angelegt, indem ich die Testdaten als Beispiel angegeben habe:

Screen Shot 2016-06-30 at 16.34.42

In der XML-Configuration wird am Endpoint ein Attribut für Metadaten ergänzt, welches ich den Beispielen hier nicht aufführe. Die ID der Metadaten ist ein zufällig generierter Wert, der selber keine Aussagekraft hat, aber auf einen Typ verweist, der in den Projekt-Metadaten verwaltet wird. Diese wiederum können über das Projekt-Kontextmenü Mule/Manage Metadata Types verwaltet werden.

Zieht man nun den DataWeave in den Flow erkennt er automatisch das Input-Format, die Beispieldaten und kann in der Preview dabei unterstützen, das korrekte Mapping zu definieren.

Screen Shot 2016-06-30 at 16.38.36

Durch die Definition des output-Formats als application/java ohne eine spezielle Klasse wird eine HashMap erzeugt. Über die hat man in der MEL jetzt direkten Zugriff auf die Elemente über deren Namen. Der Flow sieht jetzt so aus:

<flow name="dataformatsFlow">
    <http:listener config-ref="HTTP_Listener_Configuration" 
                   path="/" allowedMethods="POST" doc:name="HTTP"/>
    <dw:transform-message doc:name="HashMap">
        <dw:set-payload><![CDATA[
            %dw 1.0
            %output application/java
            ---
            payload.person
        ]]></dw:set-payload>
    </dw:transform-message>
    <set-payload value="Hallo #[payload.firstname] #[payload.name]" 
                 doc:name="Set result payload"/>
</flow>

XML als DOM

XML als String ist verhältnismäßig schwierig zu bearbeiten. Zwar kommt man mit xpath3 an die Inhalte, Manipulationen sind aber nur mit den üblichen String-Manipulationen möglich, die nicht gerade für XML-Strukturen vorgesehen sind. Wir erweitern die obige Aufgabe dahingehend, dass die Rückgabe des Aufrufs ebenfalls ein XML sein soll, und zwar so wie die Ausgangs-XML plus einem Element „greeting“:

<?xml version="1.0" encoding="UTF-8"?>
<person><name>Langmann</name><firstname>Christian</firstname><greeting>Hallo Christian Langmann</greeting></person>

Wenn man komplexe XML-Bearbeitung durchführen möchte, kann man den XML-String (oder Stream) in ein DOM (Document Object Model) überführen. Dafür gibt es den xml-to-dom-transformer. Für unsere Aufgabe schreiben wir zunächst das bisherige Ergebnis nicht mehr in die Payload, sondern in eine Variable. Dann probieren wir den xml-to-dom-transformer aus:

<flow name="dataformatsFlow">
   <http:listener config-ref="HTTP_Listener_Configuration" 
                  path="/" allowedMethods="POST" doc:name="HTTP"/>
   <set-variable variableName="greeting" 
                 value="Hallo #[xpath3(&quot;/person/firstname&quot;)] #[xpath3(&quot;/person/name&quot;)]" 
                 doc:name="set greeting"/>
   <mulexml:xml-to-dom-transformer doc:name="XML to DOM"/>
</flow>

Das reicht uns leider nicht, da diese Verwendung des xml-to-dom-transformer aus dem String/Stream lediglich ein Byte-Array erzeugt.

Screen Shot 2016-06-30 at 11.46.03

Und dieses Byte-Array ist nichts anderes als unser vorheriger String. Das kann man sehen, wenn man die Expression „new String(payload)“ zum Beispiel im Expression-Evaluator im Debugger ausführt, oder einen <byte-array-to-string-transformer> verwendet:

Screen Shot 2016-06-30 at 11.49.26

Was noch fehlt ist die konkrete Angabe, welche DOM-Implementierung gewählt werden soll.  Zur Verfügung stehen z.B. Apache Xerxes oder dom4j. Welche Implementierung verwendet werden soll wird implizit durch die „returnClass“ des xml-to-dom-transformer angegeben. Wenn ich Xerxes verwenden möchte, sieht der Transformer so aus (für dom4j gebe ich stattdessen die Klasse „org.dom4j.Document“ an):

<mulexml:xml-to-dom-transformer doc:name="XML to DOM" returnClass="org.w3c.dom.Document"/>

Im Debugger sehe ich jetzt ein DOM-Document:

Screen Shot 2016-06-30 at 14.26.20

Auf diesem Objekt kann ich wie in Java üblich arbeiten und bspw. ein neues Element einfügen. Dafür verwende ich wieder eine expression-component (je nach gewählter DOM-Implementierung sieht der Code natürlich unterschiedlich aus, siehe im Kommentarblock unten). Anschliessend muss das DOM wieder in eine lesbare Form gebracht werden, also füge ich noch einen dom-to-xml-transformer hinzu (alternativ könnte auch der xml-prettyprinter-transformer verwendet werden, der ebenfalls auf Strings, Streams, Byte-Arrays oder DOM funktioniert) :

<flow name="dataformatsFlow">
   <http:listener config-ref="HTTP_Listener_Configuration" 
                  path="/" allowedMethods="POST" doc:name="HTTP"/>
   <set-variable variableName="greeting" 
                 value="Hallo #[xpath3(&quot;/person/firstname&quot;)] #[xpath3(&quot;/person/name&quot;)]" 
                 doc:name="set greeting"/>
   <mulexml:xml-to-dom-transformer doc:name="XML to DOM" 
                                   returnClass="org.w3c.dom.Document"/>
   <expression-component doc:name="add greeting to xml"><![CDATA[
       greetingElem = payload.createElement("greeting");
       greetingElem.setTextContent(flowVars.greeting);
       payload.getDocumentElement().appendChild(greetingElem);
   ]]></expression-component>
   <!-- Code für org.dom4j.Document: 
     bNode = message.payload.rootElement.addElement('greeting'); 
     bNode.text = flowVars.greeting; 
    --> 
   <mulexml:dom-to-xml-transformer doc:name="DOM to XML"/>
</flow>

Fehlt noch der Hinweis, dass xpath3 auch auf einem DOM funktioniert. Da die Expression in der expression-component eine MEL ist, und die xpath3-Funktion auch auf einem DOM funktioniert, könnte man natürlich auch den xpath3-Aufruf direkt in die expression-component aufnehmen, so dass man die Variable nicht mehr braucht:

<flow name="dataformatsFlow">
   <http:listener config-ref="HTTP_Listener_Configuration" 
                  path="/" allowedMethods="POST" doc:name="HTTP"/>
   <mulexml:xml-to-dom-transformer doc:name="XML to DOM" 
                                   returnClass="org.w3c.dom.Document"/>
   <expression-component doc:name="add greeting to xml"><![CDATA[
        greetingElem = payload.createElement("greeting");
        greetingElem.setTextContent("Hallo " + xpath3("/person/firstname") 
                                    + " " + xpath3("/person/name"));
        payload.getDocumentElement().appendChild(greetingElem);
   ]]></expression-component>
   <mulexml:dom-to-xml-transformer doc:name="DOM to XML"/>
</flow>

Und nochmal DataWeave

Am einfachsten wäre es, das greeting-Element ohne weitere Schritte in das XML-Dokument einzufügen. Mit dem DataWeave geht auch das. Tatsächlich stellt man immer wieder fest, dass man auf viele der Standardtransformatoren verzichten kann, wenn man den DataWeave einsetzt:

Screen Shot 2016-06-30 at 16.52.27

In diesem Fall setzen wir das Ausgabeformat auf „application/xml“, übernehmen die zwei Felder und fügen das greeting-Element hinzu. Man beachte das ++ zum Konkatenieren von Strings:

 <flow name="dataformatsFlow">
    <http:listener config-ref="HTTP_Listener_Configuration" 
                   path="/" allowedMethods="POST" doc:name="HTTP"/>
    <dw:transform-message doc:name="add greeting">
       <dw:input-payload doc:sample="testdata.xml"/>
       <dw:set-payload><![CDATA[%dw 1.0
           %output application/xml
           ---
           person: {
              name: payload.person.name,
              firstname: payload.person.firstname,
              greeting: "Hallo " ++ payload.person.firstname ++ " " ++ payload.person.name
           }]]></dw:set-payload>
    </dw:transform-message>
 </flow>

Ausblick

In diesem Artikel haben wir verschiedene Arten der XML-Repräsentation und Bearbeitung betrachtet. Dabei sind wir davon ausgegangen, dass der XML-Stream über einen HTTP-POST in den Flow gelangt. Natürlich ist dies nicht der einzige Weg, Alternativen sind zum Beispiel Queues wie VM oder JMS, Files, der ApiKit-Router oder CXF (auch wenn da in der Regel auch wieder HTTP-POST hinter steckt).

Ein Wort noch zum Encoding: Zwar wird im XML-Dokument das verwendete Encoding definiert (in unseren Beispielen UTF-8). Allerdings sind die XML-Dokumente häufig Bestandteil einer Message, eines Attachments oder eines Streams, und diese können in einem anderen Encoding vorliegen. Insbesondere in Windows-Umgebungen kommt es gerne zu Problemen, weil dort häufig Cp1252 verwendet wird. Treten damit Probleme auf, kann Mule z.B. beim Starten über Java-Parameter (file.encoding) oder den deployment-descriptor umgestellt werden. Noch besser ist es, sich gar nicht erst auf das Systemencoding zu verlassen: Solange die XML-Nachricht nicht als String sondern als Byte-Array oder als Stream vorliegt, übernimmt der Parser die Verarbeitung des Encodings und berücksichtigt dabei das im XML-Tag angegebene.

Im nächsten Artikel werde ich mir über die Repräsentation und Verarbeitung von JSON-Dokumenten Gedanken machen. Und wie es sich für „richtige“ Entwickler gehört, werde ich dann auch Unittests in Form von MUnit verwenden.

Kommentare

  • 18. Juli 2016 von Bettie

    Really trsurwotthy blog. Please keep updating with great posts like this one. I have booked marked your site and am about to email it to a few friends of mine that I know would enjoy reading..

  • 20. Juli 2016 von jeffiepriya

    This information is impressive..I am inspired with your post writing style & how continuously you describe this topic. After reading your post,thanks for taking the time to discuss this, I feelhappy about it and I love learning more about this topic

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.