Previous Page Next Page

Recipe 16.4. Creating Generic Mapping Functions

Problem

You want to create reusable templates for performing operations on items in a node set.

Solution

The solution involves recursively processing the elements in $nodes and invoking the generic function, $func, on each element. You allow the possibility that the function specified by $func is parameterized. This parameter can be specified by $func-param. You further state that the default value of the $func-param is obtained from an attribute, @param1, in the generic functions tag. This stipulation allows the default to be a function of the specified generic:

<xsl:template name="generic:map">
  <xsl:param name="nodes" select="/.."/>
  <xsl:param name="func" select=" 'identity' "/>
  <xsl:param name="func-param1" 
      select="$generic:generics[self::generic:func and @name = $func]/@param1"/>
  <xsl:param name="i" select="1"/>
  <xsl:param name="result" select="/.."/>
  
  <xsl:choose>
    <xsl:when test="$nodes">
      <xsl:variable name="temp">
        <xsl:apply-templates 
             select="$generic:generics[self::generic:func and 
                                       @name = $func]">
            <xsl:with-param name="x" select="$nodes[1]"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$func-param1"/>
        </xsl:apply-templates>
      </xsl:variable>
   
      <xsl:call-template name="generic:map">
        <xsl:with-param name="nodes" select="$nodes[position( ) > 1]"/>
        <xsl:with-param name="func" select="$func"/>
        <xsl:with-param name="func-param1" select="$func-param1"/>
        <xsl:with-param name="i" select="$i +1"/>
        <xsl:with-param name="result" 
                        select="$result | exslt:node-set($temp)"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:apply-templates select="$result" mode="generic:map"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
   
<xsl:template match="/ | node( ) | @*" mode="generic:map">
  <node>
    <xsl:copy-of select="."/>
  </node>
</xsl:template>

You can see the effect of this process by considering the incr generic function:

  <generic:func name="incr" param1="1"/>
  <xsl:template match="generic:func[@name='incr']">
       <xsl:param name="x"/>
       <xsl:param name="param1" select="@param1"/> 
       <xsl:value-of select="$x + $param1"/>
  </xsl:template>

The incr generic function's parameter specifies the amount by which to increment and is defaulted to one. Here you create a stylesheet that maps incr across a node set of numbers, first using the default parameter and then by setting it to 10. For good measure, you also extend the set of generics to include a reciprocal function and map the numbers using that function:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="exslt" exclude-result-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
<!--Add a generic element function for computing reciprocal -->
<generic:func name="reciprocal"/>
<xsl:template match="generic:func[@name='reciprocal']">
     <xsl:param name="x"/>
     <xsl:value-of select="1 div $x"/>
</xsl:template>
   
<!--Test map functionality -->
<xsl:template match="numbers">
   
<results>
   
<incr>
  <xsl:call-template name="generic:map">
    <xsl:with-param name="nodes" select="number"/>
    <xsl:with-param name="func" select=" 'incr' "/>
  </xsl:call-template>
</incr>
<incr10>
    <xsl:call-template name="generic:map">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="func" select=" 'incr' "/>
      <xsl:with-param name="func-param1" select="10"/>
   </xsl:call-template>
</incr10>
<recip>
      <xsl:call-template name="generic:map">
        <xsl:with-param name="nodes" select="number"/>
        <xsl:with-param name="func" select=" 'reciprocal' "/>
     </xsl:call-template>
</recip>
</results> 
  
</xsl:template>
   
</xsl:stylesheet>

This results in the following output:

   <incr>
      <node>11</node>
      <node>4.5</node>
      <node>5.44</node>
      <node>78.7777</node>
      <node>-7</node>
      <node>2</node>
      <node>445</node>
      <node>2.1234</node>
      <node>8.77</node>
      <node>4.1415927</node>
   </incr>
   <incr10>
      <node>20</node>
      <node>13.5</node>
      <node>14.440000000000001</node>
      <node>87.7777</node>
      <node>2</node>
      <node>11</node>
      <node>454</node>
      <node>11.1234</node>
      <node>17.77</node>
      <node>13.1415927</node>
   </incr10>
   <recip>
      <node>0.1</node>
      <node>0.2857142857142857</node>
      <node>0.2252252252252252</node>
      <node>0.012857155714298572</node>
      <node>-0.125</node>
      <node>1</node>
      <node>0.0022522522522522522</node>
      <node>0.8901548869503294</node>
      <node>0.1287001287001287</node>
      <node>0.31830988148145367</node>
   </recip>
</results>

Discussion

Map can extract a subset of a node set that meets specified criteria. Here you use a generic function that is a predicate. You achieve the desired effect by structuring your predicates so that they return their input when the predicate is TRue and nothing when the predicate is false. For example:

  <generic:func name="less-than"/>
  <xsl:template match="generic:func[@name='less-than']">
       <xsl:param name="x"/>
       <!-- limit -->
       <xsl:param name="param1"/> 
       <xsl:if test="$x &lt; $param1"><xsl:value-of select="$x"/></xsl:if>
  </xsl:template>

You then capture the nodes of the result with a simple filtering template:

xsl:template match="/ | node( ) | @*" mode="generic:map">
  <xsl:if test="string(.)">
    <node>
      <xsl:copy-of select="."/>
    </node>
  </xsl:if>
</xsl:template>

Here is a sample of the technique in action:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="exslt" exclude-result-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!--Test map functionality -->
<xsl:template match="numbers">
   
<results>
   
<less-than-5>
  <xsl:call-template name="generic:map">
    <xsl:with-param name="nodes" select="number"/>
    <xsl:with-param name="func" select=" 'less-than' "/>
    <xsl:with-param name="func-param1" select="5"/>
  </xsl:call-template>
</less-than-5>
   
<greater-than-5>
  <xsl:call-template name="generic:map">
    <xsl:with-param name="nodes" select="number"/>
    <xsl:with-param name="func" select=" 'greater-than' "/>
    <xsl:with-param name="func-param1" select="5"/>
  </xsl:call-template>
</greater-than-5>
   
</results>
   
  
</xsl:template>
   
<xsl:template match="/ | node( ) | @*" mode="generic:map">
  <xsl:if test="string(.)">
    <node>
      <xsl:copy-of select="."/>
    </node>
  </xsl:if>
</xsl:template>

A test of this stylesheet produces the following output:

<results>
   <less-than-5>
      <node>3.5</node>
      <node>4.44</node>
      <node>-8</node>
      <node>1</node>
      <node>1.1234</node>
      <node>3.1415927</node>
   </less-than-5>
   <greater-than-5>
      <node>10</node>
      <node>77.7777</node>
      <node>444</node>
      <node>7.77</node>
   </greater-than-5>
</results>

Mapping is not limited to numerical processing. Consider the following stylesheet that finds the length of all para elements in a DocBook document:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="exslt" exclude-result-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
<!--Add a generic element function for computing reciprocal -->
<generic:func name="length"/>
<xsl:template match="generic:func[@name='length']">
  <xsl:param name="x"/>
  <xsl:value-of select="string-length($x)"/>
</xsl:template>
   
<!--Test map functionality -->
<xsl:template match="/">
   
<para-lengths>
    <xsl:call-template name="generic:map">
      <xsl:with-param name="nodes" select="//para"/>
      <xsl:with-param name="func" select=" 'length' "/>
    </xsl:call-template>
</para-lengths>
  
</xsl:template>
   
<xsl:template match="/ | node( ) | @*" mode="generic:map">
  <length>
    <xsl:copy-of select="."/>
  </length>
</xsl:template>
   
</xsl:stylesheet>

Or this example, which creates a document summary by extracting the first three sentences of each paragraph:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="exslt" exclude-result-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
<!--A generic function for extracting sentences -->
<generic:func name="extract-sentences" param1="1"/>
<xsl:template match="generic:func[@name='extract-
sentences']" name="generic:extract-sentences">
  <xsl:param name="x"/>
  <xsl:param name="param1" select="@param1"/> 
   <xsl:choose>
    <xsl:when test="$param1 >= 1 and contains($x,'.')">
      <xsl:value-of select="substring-before($x,'.')"/>
      <xsl:text>.</xsl:text>
      <xsl:call-template name="generic:extract-sentences">
        <xsl:with-param name="x" select="substring-after($x,'.')"/>
        <xsl:with-param name="param1" select="$param1 - 1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise/>
  </xsl:choose>
</xsl:template>
   
<xsl:template match="/">
   
<summary>
    <xsl:call-template name="generic:map">
      <xsl:with-param name="nodes" select="//para"/>
      <xsl:with-param name="func" select=" 'extract-sentences' "/>
      <xsl:with-param name="func-param1" select="3"/>
    </xsl:call-template>
</summary>
  
</xsl:template>
   
<xsl:template match="/ | node( ) | @*" mode="generic:map">
  <para>
    <xsl:copy-of select="."/>
  </para>
</xsl:template>
   
</xsl:stylesheet>

These examples are convoluted ways of creating results that can easily be obtained with more straightforward stylesheets. Creating stylesheets that perform transformations a node at a time is easy. For example, the summary stylesheet is more clearly implemented as follows:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
     
<xsl:template name="extract-sentences">
  <xsl:param name="text"/>
  <xsl:param name="num-sentences" select="1"/> 
   <xsl:choose>
    <xsl:when test="$num-sentences >= 1 and contains($text,'.')">
      <xsl:value-of select="substring-before($text,'.')"/>
      <xsl:text>.</xsl:text>
      <xsl:call-template name="extract-sentences">
        <xsl:with-param name="text" select="substring-after($text,'.')"/>
        <xsl:with-param name="num-sentences" select="$num-sentences - 1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise/>
  </xsl:choose>
</xsl:template>
   
<xsl:template match="/">
  <summary>
    <xsl:apply-templates select=".//para"/>
  </summary>
</xsl:template>
   
<xsl:template match="para">
  <para>
      <xsl:call-template name="extract-sentences">
        <xsl:with-param name="text" select="."/>
        <xsl:with-param name="num-sentences" select="3"/>
      </xsl:call-template>
    </para>
</xsl:template>
     
</xsl:stylesheet>

However, if in a single stylesheet you need to perform several map-like operations, then the generic implementation could result in less custom-written code.

An alternate mapping generic function is generic:map2. Rather than applying a unary function to a single node set, generic:map2 applies a binary function to the elements of two node sets and returns the resulting node set:

<xsl:template name="generic:map2">
  <xsl:param name="nodes1" select="/.."/>
  <xsl:param name="nodes2" select="/.."/>
  <xsl:param name="func" select=" 'identity' "/>
  <xsl:param name="func-param1" select="$generic:generics[self::generic:func and 
@name = $func]/@param1"/>
  <xsl:param name="i" select="1"/>
  <xsl:param name="result" select="/.."/>
  
  <xsl:choose>
    <xsl:when test="$nodes1 and $nodes2">
      <xsl:variable name="temp">
        <xsl:apply-templates 
             select="$generic:generics[self::generic:aggr-func and 
                                       @name = $func]">
            <xsl:with-param name="x" select="$nodes1[1]"/>
            <xsl:with-param name="accum" select="$nodes2[1]"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$func-param1"/>
        </xsl:apply-templates>
      </xsl:variable>
   
      <xsl:call-template name="generic:map2">
        <xsl:with-param name="nodes1" select="$nodes1[position( ) > 1]"/>
        <xsl:with-param name="nodes2" select="$nodes2[position( ) > 1]"/>
        <xsl:with-param name="func" select="$func"/>
        <xsl:with-param name="func-param1" select="$func-param1"/>
        <xsl:with-param name="i" select="$i +1"/>
        <xsl:with-param name="result" select="$result | exslt:node-set($temp)"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:apply-templates select="$result" mode="generic:map"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

generic:map2 operates on two independent node sets in parallel and thus can apply any binary generic operation to the successive nodes. As with generic:map, the utility is a function of the frequency of usage.


Previous Page Next Page