Recipe16.4.Creating Generic Mapping Functions


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.




XSLT Cookbook
XSLT Cookbook: Solutions and Examples for XML and XSLT Developers, 2nd Edition
ISBN: 0596009747
EAN: 2147483647
Year: 2003
Pages: 208
Authors: Sal Mangano

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net