1. Problem
You need to define mapping
from an input message with a flat structure to an output message
grouping elements by the values of fields in the input message.
2. Solution
The input message must have
records containing a field on which the grouping should be performed, as
well as the information that needs to be grouped together. The output
message needs to define records for each group and a field containing
the aggregated information. The input message may have a structure
similar to the XML in Listing 1.
Example 1. Sample Source Message for the XSLT Group-By Example
<ns0:Sales xmlns:ns0="http://tempURI.org"> <Sale> <Amount>100.01</Amount> <RepName>Megan</RepName> </Sale> <Sale> <Amount>200.01</Amount> <RepName>Megan</RepName> </Sale> <Sale> <Amount>10.10</Amount> <RepName>Leah</RepName> </Sale> <Sale> <Amount>2000</Amount> <RepName>Misti</RepName> </Sale> <Sale> <Amount>50.10</Amount> <RepName>Leah</RepName> </Sale> </ns0:Sales>
|
To create the mapping, follow these steps:
Click
the toolbox, and click the Advanced Functoids tab. Place a Scripting
functoid on the map surface and connect it to the record that will
contain each group in the destination message.
While the Scripting functoid is highlighted on the map surface, click the ellipsis to the right of the FunctoidScript
item in the Properties window. In the Configure Scripting Functoid
dialog box, on the Script Functoid Configuration tab, select Inline XSLT
for Script Type.
Create an xsl:for-each
element in the inline XSLT script, and define the groups to create in
the output message by selecting the unique values of the grouping field.
This statement will loop through each unique value appearing in the
input message.
Inside the xsl:for-each
element, create the record that should contain the group and the field
containing the value that must be aggregated for the group. The current() function obtains the current iteration value of the xsl:for-each element's select statement. Listing 2 shows the inline XSLT.
Example 2. Inline XSLT Group-By Script
<xsl:for-each select="//Sale[not(RepName=preceding-sibling::Sale/RepName)]/RepName"> <RepSales> <RepTotalAmount> <xsl:value-of select="sum(//Sale[RepName=current()]/Amount)"/> </RepTotalAmount> <RepName> <xsl:value-of select="current()"/> </RepName> </RepSales> </xsl:for-each>
|
This XSLT script will produce the XML message shown in Listing 3, containing one RepSales element for each sales representative with the total sales and name of the representative.
Example 3. Sample Destination Message for the XSLT Group-By Example
<ns0:SalesByRep xmlns:ns0="http://tempURI.org"> <RepSales> <RepTotalAmount>300.02</RepTotalAmount> <RepName>Megan</RepName> </RepSales> <RepSales> <RepTotalAmount>60.2</RepTotalAmount> <RepName>Leah</RepName> </RepSales> <RepSales> <RepTotalAmount>2000</RepTotalAmount> <RepName>Misti</RepName> </RepSales> </ns0:SalesByRep>
|
3. How It Works
The key feature of this inline XSLT example is the select statement appearing in the xsl:for-each element. This statement will create a list of values to create a group for, containing the distinct values of RepName in our example. Each RepName is located at //Sale/RepName in the input message. However, the select
statement should obtain only the first occurrence of each distinct
group value. This inline example achieves this by adding the filter [not(RepName=preceding-sibling::Sale/RepName)] to the select statement. The xsl:for-each element will loop through the first occurrence of each unique grouping value, and the value can be obtained within the xsl:for-each element with the current() function.
When the filter expression evaluates a Sale element, the condition RepName=preceding-sibling::Sale/RepName is true whenever there is an element appearing before it with the same RepName value. Placing the condition inside the not() function makes it true only when there are no preceding elements with the same RepName value, so it is only true for the first occurrence of each distinct RepName value.
The inline XSLT script in Listing 2
calculates the total sales of each representative and creates one
record in the output message containing the total sales and the sales
representative's name.
In addition to grouping
fields in the output message, the map may need to perform aggregations
for the groups of input message values. Perhaps BizTalk cannot determine
the specific groupings that the map needs to perform until runtime, or
static links may not be practical because of a large number of possible
groups.
When inline XSLT performs the grouping, BizTalk applies the filter expression to the //Sale statement, which means BizTalk applies the filter expression to every Sale element in the input message. For each input message, the expression checks the value of every preceding-sibling and returns true when none of the preceding-sibling elements has the same RepName value. This algorithm is not efficient for large messages.
There is a generally
more efficient alternative XSLT approach to the group by problem. This
alternative approach is the Muenchian method. The Muenchian method is
generally more efficient than the inline solution presented here, but
the default BizTalk functoids cannot implement it. The Muenchian method
declares an xsl:keyRepName with an xsl:key,
eliminating the computational cost of checking every preceding sibling
incurred with inline XSLT. However, since the top level of the xsl:stylesheet element must declare the xsl:key
element, inline XSLT cannot implement it. Only a separate style sheet
file can implement the Muenchian method. Place the XSLT style sheet in Listing 3-43 in a separate file for this example. element at the top level of the XSLT style sheet. The map directly obtains a node set for each distinct
Example 3.43. Sample Group-By Style Sheet
<?xml version="1.0"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:key name="SalesByRepKey" match="Sale" use="RepName"/> <xsl:template match="/"> <ns0:SalesByRep xmlns:ns0='http://tempURI.org'> <xsl:apply-templates select="//Sale[generate-id()= generate-id(key('SalesByRepKey', RepName)[1])]"/> </ns0:SalesByRep> </xsl:template> <xsl:template match="Sale"> <RepSales> <RepTotalAmount>
<xsl:value-of select="sum(key('SalesByRepKey', RepName)/Amount)" /> </RepTotalAmount> <RepName> <xsl:value-of select="RepName" /> </RepName> </RepSales> </xsl:template> </xsl:stylesheet>
|
Specify the external XSLT file used by a BizTalk map (see the next recipe for steps on doing this).