Skip to main content

Tip: Packaging XSLT lookup tables as EXSLT functions

Use the community standard XSLT extensions

Uche Ogbuji (uche@ogbuji.net), Principal Consultant, Fourthought, Inc.
Photo of Uche Ogbuji
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.

Summary:  In an earlier tip, Uche Ogbuji demonstrated how to build lookup tables in XSLT. In a follow-up tip, he covered how to handle error or default conditions for these lookup tables. This tip shows you how to use the functions module from EXSLT, the community standard in XSLT extensions, and how this technique further improves lookup tables by packaging code into easily reusable functions.

View more content in this series

Date:  24 Jan 2005
Level:  Intermediate
Activity:  1382 views

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).


EXSLT to the rescue

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.


Wrap-up

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.


Resources

About the author

Photo of Uche Ogbuji

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)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML
ArticleID=33327
ArticleTitle=Tip: Packaging XSLT lookup tables as EXSLT functions
publish-date=01242005
author1-email=uche@ogbuji.net
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers