Sortiere Ausgabe im Mapper

Einzelne Felder in der Ausgabe zu sortieren scheint eine gewöhnliche und vor allem alltägliche Aufgabe zu sein. Ist an sich auch unproblematisch innerhalb des BizTalk-Mappers. Im Looping-Functoid gibt man die Parameter einfach in der gewünschten Reihenfolge an.

Aber das geht nur, so lange man verschiedene Eingabe-Records nicht “mischen” will.
Ein kleines Beispiel:

1
2
3
4
5
6
7
8
9
10
11
<root>
<nodes>
<node id="1" />
<node id="2" />
</nodes>
<subnodes>
<subnode parent="1" />
<subnode parent="1" />
<subnode parent="2" />
</subnodes>
</root>

Natürlich sollen in der Ausgabe nach der node mit der id 1 die dazu passenden subnodes erscheinen.
Und hier wirds nun schwerer - um nicht zu sagen mit den Basis-Functoiden (ausgenommen dem Script-Functioid) unmöglich.

Wenn man einfach nur sein Looping-Functoid wie gewohnt mit den Records nodes und subnodes verbindet, erscheint in der Ausgabe (wie erwartet)

1
2
3
4
5
6
7
<root>
<NodeOrSubNode id="1" />
<NodeOrSubNode id="2" />
<NodeOrSubNode parent="1" />
<NodeOrSubNode parent="1" />
<NodeOrSubNode parent="2" />
</root>

Nun - im Grunde es ist wie bei meinem Post Table Looping zusammen mit Logical Functoid - im besten Fall könnte man die SourceLink - Eigenschaft ändern und der Map-Compiler macht was draus. Tut er aber nicht :( Und leider hab ich hier noch keine Möglichkeit der XPath-Injection gefunden (nicht, dass ich nicht gesucht hätte *g*).
Das Problem ist die Art und Weise, wie der Map-Compiler das XSL-Dokument und speziell das Looping Functoid zusammensetzt.

Wenn der Eingangsparameter ein einzelnes Feld ist, dann wird dieses einfach ausgegeben. Ist es dagegen ein Record, dann wird noch ein <xsl:for-each>-Element außenrum gesetzt (es sei denn MinOccurs und MaxOccurs des Records sind jeweils auf 1 gesetzt).
Das ist soweit noch OK, aber der Knackpunkt ist, dass jeder Eingangsparameter von den anderen getrennt wird - so kann es zu keiner Sortierung kommen.

Obiges Beispiel sieht z.B. so aus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<xsl:for-each select="nodes">
<xsl:for-each select="node">
<NodeOrSubnode>
<xsl:if test="@id">
<xsl:attribute name="id">
<xsl:value-of select="@id" />
</xsl:attribute>
</xsl:if>
</NodeOrSubnode>
</xsl:for-each>
</xsl:for-each>
<xsl:for-each select="subnodes">
<xsl:for-each select="subnode">
<NodeOrSubnode>
<xsl:if test="@parent">
<xsl:attribute name="parent">
<xsl:value-of select="@parent" />
</xsl:attribute>
</xsl:if>
</NodeOrSubnode>
</xsl:for-each>
</xsl:for-each>

Zwei getrennte <xsl:for-each>-Blöcke “interdisziplinär” zu sortieren geht einfach nicht. Und das ist dann leider auch des Rätsels Lösung - es gibt keine (innerhalb des Mappings).

Naja, das wäre gelogen. Es gibt doch eine Lösung, die aber nur greift, wenn man die Elemente im Ursprungsdokument bereits sortiert hat. Dann kann man in der *.btm-Datei (als XML-Dokument öffnen) das Attribut PreserveSequenceOrder=Yes innerhalb des mapsource-Elements angeben und die Trennung wird aufgehoben. Stattdessen werden die beiden Blöcke dann zu

1
<xsl:for-each select="node|subnode">

In meinem Fall ist die Eingabe aber nicht korrekt sortiert und daher hilft es nur wenig - denn wie gesagt, XPath-Injection kann hier nicht verwendet werden. In einem größeren Projekt wurden bei mir sogar (bisher) unerklärliche Fehler ausgegeben, sobald die Eigenschaft auf Yes gesetzt wurde.

Zurück zum Fest: irgendwie sortieren kann man natürlich trotz allem - allerdings nur in einem weiteren Mapping. Dieses wird bestenfalls mit Hilfe einer externen XSL-Datei zu umgesetzt (oder wahlweise mit einem Scripting-Functoid und Inline XSLT). Hier folgt ein recht universelles Beispiel (d.h. es sind nur wenig Anpassungen notwendig um damit arbeiten zu können).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:ns0="http://Test.Sort" version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="no" version="1.0" method="xml" standalone="yes" />
<!-- Standard, kann für alle XML-Dokumente gleich bleiben -->
<xsl:template match="*" priority="0">
<xsl:call-template name="element" />
</xsl:template>
<xsl:template name="element">
<xsl:element name="{name(.)}">
<xsl:apply-templates select="@*" />
<xsl:value-of select="text()"/>
<xsl:apply-templates select="*" />
</xsl:element>
</xsl:template>
<xsl:template match="@*">
<xsl:attribute name="{name(.)}">
<xsl:value-of select="."/>
</xsl:attribute>
</xsl:template>
<!-- Sortierung eines bestimmten Elements innerhalb einer Ebene -->
<xsl:template match="NodeOrSubnode" priority="1">
<!-- beim ersten Auftreten des zu sortierenden Elements, alle NodeOrSubNode-Elemente auf der gleichen Ebene ausgeben -->
<xsl:if test="not(preceding-sibling::NodeOrSubnode)">
<xsl:for-each select="../NodeOrSubnode[@id]">
<!--nach was soll sortiert werden-->
<xsl:sort select="@id" data-type="number" />
<xsl:call-template name="element" />
<xsl:variable name="id" select="@id" />
<!-- auch Unterelemente ausgeben -->
<xsl:for-each select="../NodeOrSubnode[@parent=$id]">
<!-- nach was soll sortiert werden (in diesem Beispiel nicht wirklich sinnvoll!) -->
<xsl:sort select="@parent" data-type="number" />
<xsl:call-template name="element" />
</xsl:for-each>
</xsl:for-each>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

Vorgehensweise

  • SortNodes.zip downloaden
  • Anpassen (Namespace und xsl:templates zum Sortieren)
  • Speichern
  • neues Mapping einfügen
  • 2x das gleiche Schema auswählen und ins leere Grid klicken
  • nun kann man die Custom XSL Path-Eigenschaft festlegen (mit dem Dateipfad zur obigen Datei)
  • Datei durchjagen. Fertig

Nachdem das Dokument nun durch beide Mappings gelaufen ist, sieht es endlich so aus, wie es aussehen soll. Endlich ;)

1
2
3
4
5
6
7
<root>
<NodeOrSubnode id="1" />
<NodeOrSubnode parent="1" />
<NodeOrSubnode parent="1" />
<NodeOrSubnode id="2" />
<NodeOrSubnode parent="2" />
</root>