In the two tips, "XSLT lookup tables" and "Default and error handling in XSLT lookup tables," I showed how to create XSLT code to look up values provided statically to the processor, including support for default values or error handling. The resulting code for handling all aspects of this lookup was straightforward but rather unwieldy, especially if you happen to have multiple tables that you want to use throughout a transform. Listing 1 is the example implementation (from the most recent tip) of lookup tables that can provide a default value when the item being looked up is not found.
Listing 1. Lookup table example with default value for lookup (states-lookup-default.xslt)
<?xml version="1.0"?>
<xsl:transform
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:s="http://example.com/states.data"
version="1.0"
>
<xsl:output method="text"/>
<xsl:key name="state-lookup" match="s:state" use="s:abbr"/>
<xsl:variable name="states-top" select="document('')/*/s:states"/>
<xsl:template match="label">
<xsl:value-of select="name"/>
<xsl:text> of </xsl:text>
<xsl:apply-templates select="$states-top">
<xsl:with-param name="curr-label" select="."/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="s:states">
<!-- This template updated to add a default value signal -->
<xsl:param name="curr-label"/>
<xsl:variable name="look-for" select="$curr-label/address/state"/>
<xsl:variable name="default" select="s:default"/>
<xsl:variable name="result"
select="key('state-lookup', $look-for)/s:name"/>
<xsl:choose>
<xsl:when test="$result">
<xsl:value-of select="$result"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$default"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<s:states>
<s:state><s:abbr>CO</s:abbr><s:name>Colorado</s:name></s:state>
<s:state><s:abbr>CT</s:abbr><s:name>Connecticut</s:name></s:state>
<s:state><s:abbr>ID</s:abbr><s:name>Idaho</s:name></s:state>
<s:state><s:abbr>NJ</s:abbr><s:name>New Jersey</s:name></s:state>
<!-- Added default value -->
<s:default><s:name>[UNKNOWN]</s:name></s:default>
</s:states>
</xsl:transform>
|
Limits of modularization in XSLT 1.0
You might try to clean up this code a bit by packaging the key lookup code in a named template. Listing 2 is an attempt at this; it doesn't quite work, but probably comes as close as possible in XSLT 1.0 without additional complications (or without restrictive limitations on the sorts of lookup performed).
Listing 2. Attempt to package lookup code in a reusable template
<?xml version="1.0"?>
<xsl:transform
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:s="http://example.com/states.data"
version="1.0"
>
<xsl:output method="text"/>
<xsl:key name="state-lookup" match="s:state" use="s:abbr"/>
<xsl:variable name="states-top" select="document('')/*/s:states"/>
<xsl:template match="label">
<xsl:value-of select="name"/>
<xsl:text> of </xsl:text>
<xsl:call-template name="lookup">
<xsl:with-param name="key-name" select="'state-lookup'"/>
<xsl:with-param name="look-for" select="address/state"/>
<xsl:with-param name="default" select="$states-top/s:default"/>
<xsl:with-param name="table-doc" select="$states-top"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="lookup">
<xsl:param name="key-name"/> <!-- name of XSLT key -->
<xsl:param name="look-for"/> <!-- what to look up -->
<xsl:param name="default"/>
<!-- Node set whose first item can be used to set the proper context
for key lookup. -->
<xsl:param name="table-doc"/>
<!-- Force context to document where the lookup table is defined -->
<xsl:for-each select='$table-doc[1]'>
<xsl:variable name="result" select="key($key-name, $look-for)"/>
<xsl:choose>
<xsl:when test="$result">
<xsl:value-of select="$result"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$default"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<s:states>
<s:state><s:abbr>CO</s:abbr><s:name>Colorado</s:name></s:state>
<s:state><s:abbr>CT</s:abbr><s:name>Connecticut</s:name></s:state>
<s:state><s:abbr>ID</s:abbr><s:name>Idaho</s:name></s:state>
<s:state><s:abbr>NJ</s:abbr><s:name>New Jersey</s:name></s:state>
<!-- Added default value -->
<s:default><s:name>[UNKNOWN]</s:name></s:default>
</s:states>
</xsl:transform>
|
The main issue in Listing 2 is that the lookup is always rendered in the lookup template by taking the string value of $result. But this is not what I want. If the label's state is given as "CO", I want "Colorado" to be displayed. In Listing 2, the $result is "COColorado" because the key result is the entire matching s:state element. Compare this to Listing 1 where the result of key lookup is immediately narrowed down to the desired string by selecting s:name children.
Different cases for lookup require different XPath expressions to extract the specific result from the node that's returned from the XSLT key. To solve the problem I described in the last paragraph, you might try to somehow pass in to the lookup template an XPath expression to be used on the XSLT key result. However, doing so requires the ability to specify an XPath dynamically, which is not provided for in XSLT 1.0. Alternatively, you might attempt to wrap the xsl:call-template in an xsl:variable and then get the desired s:name child from this variable. But this won't work either because the variable that's created will always evaluate to a result-tree fragment (RTF), and XSLT 1.0 does not allow you to perform axis operations against an RTF.
Listing 2 highlights other, less significant problems as well. The whole xsl:call-template construct is unwieldy and verbose. The behavior of XSLT keys requires the somewhat confusing need to use xsl:for-each to force the context to a node in the document with the lookup table, which is often not the same as the source document (in these examples, it is the stylesheet document itself).
By using EXSLT, you can elegantly solve all the problems I've described except for the last one. The code in Listing 3 correctly emulates the behavior of Listing 1, while using a couple of EXSLT functions to solve the problems I've described. It also allows for neat packaging.
Listing 3. Properly modularized version of Listing 1, using EXSLT
<?xml version="1.0"?>
<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:s="http://example.com/states.data"
xmlns:func="http://exslt.org/functions"
xmlns:dyn="http://exslt.org/dynamic"
extension-element-prefixes="func"
>
<xsl:output method="text"/>
<xsl:key name="state-lookup" match="s:state" use="s:abbr"/>
<xsl:variable name="states-top" select="document('')/*/s:states"/>
<xsl:template match="label">
<xsl:value-of select="name"/>
<xsl:text> of </xsl:text>
<xsl:value-of
select="s:lookup('state-lookup', address/state,
$states-top/s:default, $states-top,
'$result/s:name')"/>
</xsl:template>
<func:function name="s:lookup">
<xsl:param name="key-name"/> <!-- name of XSLT key -->
<xsl:param name="look-for"/> <!-- what to look up -->
<xsl:param name="default"/>
<!-- Node set whose first item can be used to set the proper context
for key lookup. -->
<xsl:param name="table-doc" select="$default"/>
<!-- A string containing an XPath expression to be evaluated to
get the final result. By default, just render the XSLT key
result as is -->
<xsl:param name="result-expr" select="'$result'"/>
<!-- Force context to document where the lookup table is defined -->
<xsl:for-each select='$table-doc[1]'>
<xsl:variable name="result" select="key($key-name, $look-for)"/>
<xsl:choose>
<xsl:when test="$result">
<func:result select="dyn:evaluate($result-expr)"/>
</xsl:when>
<xsl:otherwise>
<func:result select="$default"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</func:function>
<s:states>
<s:state><s:abbr>CO</s:abbr><s:name>Colorado</s:name></s:state>
<s:state><s:abbr>CT</s:abbr><s:name>Connecticut</s:name></s:state>
<s:state><s:abbr>ID</s:abbr><s:name>Idaho</s:name></s:state>
<s:state><s:abbr>NJ</s:abbr><s:name>New Jersey</s:name></s:state>
<!-- Added default value -->
<s:default><s:name>[UNKNOWN]</s:name></s:default>
</s:states>
</xsl:transform>
|
As desired, the user-defined function s:lookup is reusable for most uses of lookup. You can see from the first template how the briefer XPath function syntax simplifies the code requesting the lookup. In the function invocation, the order of parameters passed in corresponds to the the order of xsl:params in the function definition. The func:result extension is used to determine the value to be passed back to the caller. In other respects, the user-defined function body behaves similarly to an XSLT 1.0 template. You actually have a great deal of flexibility with this technique. The following snippet is another way of invoking the function, and works just as well:
<xsl:value-of
select="s:lookup('state-lookup', address/state,
$states-top/s:default, $states-top')/s:name"/>
|
Notice that user-defined functions let you neatly side-step XSLT's RTF restrictions.
You can find EXSLT extensions that help with many of XSLT 1.0's weaknesses and limitations. In this tip, you have learned how to use EXSLT to simplify an already useful XSLT 1.0 technique.
- Review the precursor tips "XSLT lookup tables" (developerWorks, February 2001) and "Default and error handling in XSLT lookup tables" (developerWorks, December 2004).
- Learn more about EXSLT by reading Uche Ogbuji's article "EXSLT by example" (developerWorks, February 2003). All the modules, elements, and functions are detailed at EXSLT.org. In particular, see the EXSLT functions and dynamic modules. There is also information on contributing your own additions to EXSLT. For a discussion of EXSLT, join the EXSLT mailing list.
- Find more XML resources on the developerWorks XML zone. For a complete list of XML tips to date, check out the tips summary page.
-
Browse for books on these and other technical topics.
- Find out how you can become an IBM Certified Developer in XML and related technologies.
-
IBM trial software: Build your next development project with trial software available for download directly from developerWorks.

Uche Ogbuji is a consultant and co-founder of Fourthought Inc., a software vendor and consultancy specializing in XML solutions for enterprise knowledge management. Fourthought develops 4Suite, an open source platform for XML, RDF, and knowledge-management applications. Mr. Ogbuji is also a lead developer of the Versa RDF query language. He is a computer engineer and writer born in Nigeria, living and working in Boulder, Colorado, USA. You can contact Mr. Ogbuji at uche@ogbuji.net.
Comments (Undergoing maintenance)





