Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
udo_martens
Active Contributor

Background

One of the most grave handicaps of XSLT is, that there are no real variables like in other languages. For some tasks, like in the following example, it would be desirable to have them. There is an indirect way to create them by recursive templates.

Example

A Stylesheet should sort datasets by an ID. We would like to list all single entries and as well all double entries in separated containers.


Source Example

<?xml version="1.0" encoding="UTF-8"?>

<source>

<row>

  <id>001</id>

  <value>1000</value>

</row>

<row>

  <id>002</id>

  <value>1020</value>

</row>

<row>

  <id>003</id>

  <value>980</value>

</row>

<row>

  <id>002</id>

  <value>1020</value>

</row>

<row>

  <id>002</id>

  <value>1040</value>

</row>

</source>

Target

<?xml version="1.0" encoding="UTF-8"?>

<target>

<singleRows>

  <row>

   <id>001</id>

   <value>1000</value>

  </row>

  <row>

   <id>003</id>

   <value>980</value>

  </row>

</singleRows>

<doubleRows>

  <row>

   <id>002</id>

   <value>1020</value>

  </row>

</doubleRows>

</target>

Lets define the rules that

  • single IDs should be put in container "singleRows"
  • IDs which raise more that one time should be put in container "doubleRows
  • every ID should be listed only once
  • the stylesheet has to work even if the source document is not sorted by ID

The Mapping Programm

Conventional Mapping Logic

A traditional program would now

  1. sort the rows by ID
  2. loop over the rows
  3. remember last ID
  4. compare with next ID
  5. fill arrays
  6. delete double entries
  7. and prepare the output

Stylesheet Logic

A Stylesheet is not able to work in that way, we don't have variables or arrays. A solution can be to create a recursive template, that means a template, which is calling itself. The trick is to call it with parameters, which can be used in the role of variables.

 
 

The Parameters

Which Parameters should be given to the recursive call?

  • ID: We need to know, which ID was in the last row
  • POSITION: We need to know the position of the last row in the loop
  • COUNT: We need to know how many times the last ID was repeated
  • CONTAINER: We need to know in which container we are putting the output

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">

<xsl:template match="/">

  <target>

   <singleRows>

    <xsl:call-template name="recursive">

     <xsl:with-param name="ID" select="0"/>

     <xsl:with-param name="POSITION" select="0"/>

     <xsl:with-param name="COUNT" select="0"/>

     <xsl:with-param name="CONTAINER" select="'single'"/>

    </xsl:call-template>

   </singleRows>

   <doubleRows>

    <xsl:call-template name="loop">

     <xsl:with-param name="ID" select="0"/>

     <xsl:with-param name="POSITION" select="0"/>

     <xsl:with-param name="COUNT" select="0"/>

     <xsl:with-param name="CONTAINER" select="'double'"/>

    </xsl:call-template>

   </doubleRows>

  </target>

</xsl:template>

<xsl:template name="recursive">

  <xsl:param name="ID"/>

  <xsl:param name="POSITION"/>

  <xsl:param name="COUNT"/>

  <xsl:param name="CONTAINER"/>

  <xsl:for-each select="//row">

                  <!--LOGIC-->

  </xsl:for-each>

</xsl:template>

</xsl:stylesheet>

We start with an outer template, which is calling the recursive with null-values / the values for the container to be filled. Inside the recursive template we implement a loop over all row elements.

Sorting of rows

This task is quite easy. We can manage that with:

<xsl:for-each select="//row">

<xsl:sort select="id"/>

  ...

Program Logic

To look at any row only one time we ask for the current position in the loop and compare with the last position + 1


Now we compare the current ID with the last ID to find out, if ID is new or not.
Case the ID is new we call the template (after an output) again with COUNT=1, else we increment the COUNT.  

The Loop without Output

<xsl:for-each select="//row">

<xsl:sort select="id"/>

<xsl:if test="position() = $POSITION+1">

  <xsl:choose>

   <xsl:when test="id!=$ID">

    <!--OUTPUT-->

    <xsl:call-template name="recursive">

     <xsl:with-param name="ID" select="id"/>

     <xsl:with-param name="POSITION" select="position()"/>

     <xsl:with-param name="COUNT" select="1"/>

     <xsl:with-param name="CONTAINER" select="$CONTAINER"/>

    </xsl:call-template>

   </xsl:when>

   <xsl:otherwise>

    <!--OUTPUT-->

    <xsl:call-template name="recursive">

     <xsl:with-param name="ID" select="id"/>

     <xsl:with-param name="POSITION" select="position()"/>

     <xsl:with-param name="COUNT" select="$COUNT+1"/>

     <xsl:with-param name="CONTAINER" select="$CONTAINER"/>

    </xsl:call-template>

   </xsl:otherwise>

  </xsl:choose>

</xsl:if>

</xsl:for-each>

Conditions for Single Output

If we have a new ID in the loop, the old one has to be printed, but only if it was counted only once -> if COUNT=1.
We do that only for the container "single".

We fill the field "id" with the old ID,
the corresponding field "value" must be filled with the last value by a X-Path expression.

Finally we shouldn't forget the last row, which can't be printed by the next row.
 

The Output for Single IDs

<xsl:if test="$COUNT=1 and $CONTAINER='single'">

<row>

  <id>

  <xsl:value-of select="$ID"/>

  </id>

  <value>

  <xsl:value-of select="../row/value[../id=$ID]"/>

  </value>

</row>

</xsl:if>

<xsl:if test="position()=last() and $CONTAINER='single'">

<xsl:copy-of select="."/>

</xsl:if>

Conditions for Double Output

The double Output is a bit easier.

We know that there is an old ID in the loop.
If we fould it second time - COUNT=1 - we print it. We do that only for the container "double".

Because we use the current row we can just copy into the target.  

The Output for Double IDs

<xsl:if test="$COUNT=1 and $CONTAINER='double'">

<xsl:copy-of select="."/>

</xsl:if>

The complete Source Code:

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">

<xsl:template match="/">

  <target>

   <singleRows>

    <xsl:call-template name="recursive">

     <xsl:with-param name="ID" select="0"/>

     <xsl:with-param name="POSITION" select="0"/>

     <xsl:with-param name="COUNT" select="0"/>

     <xsl:with-param name="CONTAINER" select="'single'"/>

    </xsl:call-template>

   </singleRows>

   <doubleRows>

    <xsl:call-template name="recursive">

     <xsl:with-param name="ID" select="0"/>

     <xsl:with-param name="POSITION" select="0"/>

     <xsl:with-param name="COUNT" select="0"/>

     <xsl:with-param name="CONTAINER" select="'double'"/>

    </xsl:call-template>

   </doubleRows>

  </target>

</xsl:template>

<xsl:template name="recursive">

  <xsl:param name="ID"/>

  <xsl:param name="POSITION"/>

  <xsl:param name="COUNT"/>

  <xsl:param name="CONTAINER"/>

  <xsl:for-each select="//row">

   <xsl:sort select="id"/>

   <xsl:if test="position() = $POSITION+1">

    <xsl:choose>

     <xsl:when test="id!=$ID">

      <xsl:if test="$COUNT=1 and $CONTAINER='single'">

       <row>

        <id>

         <xsl:value-of select="$ID"/>

        </id>

        <value>

         <xsl:value-of select="../row/value[../id=$ID]"/>

        </value>

       </row>

      </xsl:if>

      <xsl:if test="position()=last() and $CONTAINER='single'">

       <xsl:copy-of select="."/>

      </xsl:if>

      <xsl:call-template name="recursive">

       <xsl:with-param name="ID" select="id"/>

       <xsl:with-param name="POSITION" select="position()"/>

       <xsl:with-param name="COUNT" select="1"/>

       <xsl:with-param name="CONTAINER" select="$CONTAINER"/>

      </xsl:call-template>

     </xsl:when>

     <xsl:otherwise>

      <xsl:if test="$COUNT=1 and $CONTAINER='double'">

       <xsl:copy-of select="."/>

      </xsl:if>

      <xsl:call-template name="recursive">

       <xsl:with-param name="ID" select="id"/>

       <xsl:with-param name="POSITION" select="position()"/>

       <xsl:with-param name="COUNT" select="$COUNT+1"/>

       <xsl:with-param name="CONTAINER" select="$CONTAINER"/>

      </xsl:call-template>

     </xsl:otherwise>

    </xsl:choose>

   </xsl:if>

  </xsl:for-each>

</xsl:template>

</xsl:stylesheet>

Labels in this area