Keyed lookups in XSLT 1.0

Thursday 23 June 2005

I write a lot of XSLT for a variety of reasons, but somehow the "key" function has always baffled me. Every time I need to use it, I scour Google, and re-read my own XSLT code, trying to find examples that will help me, once again, understand how it works.

Recently I needed a simple string lookup function, and used document("") and key() to build it, but it was again a process of head-scratching trial and error. I got it to work, and I finally understand it. Here are the results, offered in the hope that they will aid some future fellow craftsman.

I'll present a simplified example of using a lookup table embedded in the stylesheet itself. I'll describe each hunk of code as we go along, and then show the whole thing put together.

My lookup table is going to be embedded in the stylesheet. So that it won't be interpreted by the XSLT engine, we'll put the table in a different namespace. The stylesheet element defines the namespace ("lookup:"), and declares it as an extension namespace so that it won't appear in the output. You should use a different URL than "yourdomain", and remember, it doesn't have to actually resolve to something:

<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:lookup="http://yourdomain.com/lookup"
    extension-element-prefixes="lookup"
    version="1.0">

Then I create the lookup table. It's an ad-hoc XML structure, at the top level of the stylesheet. Here I'm going to look up strings by an id, and I'll use an id= attribute, with the string in the text of the element:

<lookup:strings>
    <string id='foo'>Fooey</string>
    <string id='bar'>Barbie</string>
</lookup:strings>

I use a <key> element to declare the key. This is where it starts becoming non-intuitive. The only way to understand the key feature of XSLT is to look at two parts at once: the key is declared with a <key> element, and then accessed with the key() function. The part that always throws me is that I expect the key definition to specify some source data: it does not. The key definition specifies what I think of as a hypothetical set of nodes, and a way to index into them. Later, when you use the key() function, you apply this definition to a real chunk of data.

The parts of a <key> element are:

  • The name attribute: simply a name for the key definition, choose anything you want. You'll use this later in the key() function to refer to the definition.
  • The match attribute: this selects a set of nodes from the data to be named later.
  • The use attribute: this specifies, for each node matched by the match attribute, the lookup value to use.

Here's my key definition:

<xsl:key name='string' match='lookup:strings/string' use='@id' />

The name is "string", the "match" attribute says to consider any <string> element that is a child of a <lookup:strings> element, and the "use" attribute says that for each such <string> element, we'll use its "id" attribute as its tag. Think of the nodes selected by the "match" attribute as the records in the table, and the value on each selected by the "use" attribute as the indexed value in the record.

Now the key is defined, and we can actually use it with the key() function. It takes two arguments: the name of the key (from the name attribute of the <key> definitions), and the value to actually look up in the table. Remember we were going to specify the actual table data with the key() function, right? Well, not really. The table data is actually the current context node. That is, the records in the table are found by applying the <key>'s "match" attribute as a pattern against the current node. Here's where the match attribute on the <key> element becomes so important. You have to carefully consider what your current context is, and design the key declaration to work within it.

In this case, we'll use the document("") function to read the current stylesheet, finding the <lookup:strings> element in it. A <for-each> element changes the current context to the table. Normally, <for-each> is used to apply a template to a number of nodes. Here, we know there is only one, but <for-each> has the handy property of setting the current node. Then the key() function can apply the <key> match pattern to find the candidate records, using our supplied value ("foo") to find a record with an id attribute of "foo":

<xsl:template match='blah'>
    <!-- Look up the string "foo" and use it. -->
    <xsl:for-each select='document("")//lookup:strings'>
        <xsl:value-of select='key("string", "foo")'/>
    </xsl:for-each>
</xsl:template>

For repetitive use, you can define a variable to hold the table, and then use it from the variable each time:

<xsl:variable name='strings' select='document("")//lookup:strings' />

<xsl:template match='blah'>
    <xsl:for-each select='$strings'>
        <xsl:value-of select='key("string", "foo")'/>
    </xsl:for-each>
</xsl:template>

Finally, here's a complete example:

<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:lookup="http://yourdomain.com/lookup"
    extension-element-prefixes="lookup"
    version="1.0">

<lookup:strings>
    <string id='foo'>Fooey</string>
    <string id='bar'>Barbie</string>
</lookup:strings>

<xsl:key name='string' match='lookup:strings/string' use='@id' />

<xsl:variable name='strings' select='document("")//lookup:strings' />

<xsl:template match='/'>
    <xsl:for-each select='$strings'>
        <xsl:value-of select='key("string", "foo")'/>
    </xsl:for-each>
</xsl:template>

</xsl:stylesheet>

When run on any input, this produces:

Fooey

Whew! No one ever claimed XSLT was succinct!

You might also want to look at:

  • The <xsl:key>s to Happiness provides another walk-through of a similar problem.
  • XSLT 2.0 adds a lot of features that should make this easier. It does away with the need for document("") and <for-each> by letting you declare the table as a variable, and then using the variable as a third argument to the key() function directly.
tagged: » 5 reactions

Comments

[gravatar]
freddy mac 9:03 PM on 23 Jun 2005

This is good stuff Ned. Whenever I am using XSLT, I always have some spec. is going to come along and lay waste to what I already know and how I use it, which I never feel elsewhere...

[gravatar]
Frederik 10:18 PM on 29 Mar 2008

Hi,
I tried similar (beginner with xsl) for taking a specified value (countryID) and lookup some countryCode for that out of another xml file containing of ID elements and assigned names.
I tried using xsl:key... and always failed. Finally, after trying I found the following. Kindly asking you for dropping me a note if this is fine in your eyes:

<xsl:stylesheet [...]>

  <xsl:variable name="docLookupCountry" select="document('countryOut.xml')/country"/>
  [...]
  <xsl:template match="countryID">
    <countryCode>
      <xsl:value-of select="$docLookupCountry[id=current()]/name" />
    </countryCode>
  </xsl:template>
</xsl:stylesheet>

well, it works!! With only one line for loading the external file, and one single line for finding the lookup-value.

Greetings.....

[gravatar]
Sebastien 2:58 PM on 21 Aug 2012

However hard I try, I can never get your example to work. My currently hard coded looked up field, ship_to_method, is always empty :-(

Do you see anything wrong? I turned this upside down multiple times. No luck!

INPUT

<Order>
  <OrderInfo>
    <CartUserInfo>
      <FirstName>John</FirstName>
      <LastName>Doe</LastName>
    </CartUserInfo>
    <Fulfillment>
      <ShippingMethod>Fedex(Standard Overnight)</ShippingMethod>
    </Fulfillment>
  </OrderInfo>
</Order>
XLST
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:lookup="http://yourdomain.com/lookup" extension-element-prefixes="lookup" version="1.0">
		
	<lookup:strings>
		<string id="Fedex(Priority Overnight)">FP</string>
		<string id="Fedex(Fedex 2 Day)">F2</string>
		<string id="Fedex(Standard Overnight)">F1</string>
		<string id="Fedex(Fedex Ground)">FG</string>
		<string id="Fedex(International Priority)">I1</string>
	</lookup:strings>
	
	<xsl:key name='string' match='lookup:strings/string' use='@id' />
	
	<xsl:variable name='strings' select='document("")//lookup:strings' />
	
	<xsl:template match="/Order">
		<order>
			<ship_to_info>
				<ship_to_fname>
					<xsl:value-of select="string(OrderInfo/CartUserInfo/FirstName)"/>
				</ship_to_fname>
				<ship_to_lname>
					<xsl:value-of select="string(OrderInfo/CartUserInfo/LastName)"/>
				</ship_to_lname>
				<ship_to_method>
					<xsl:for-each select='$strings'>
						<xsl:value-of select='key("string", "Fedex(Priority Overnight)")'/>
					</xsl:for-each>
				</ship_to_method>
			</ship_to_info>
		</order>
	</xsl:template>
</xsl:stylesheet>
OUTPUT
<?xml version="1.0" encoding="utf-8"?>
<order xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <ship_to_info>
    <ship_to_fname>John</ship_to_fname>
    <ship_to_lname>Doe</ship_to_lname>
    <ship_to_method />
  </ship_to_info>
</order>

[gravatar]
Stephen M 6:06 PM on 18 Feb 2013

Very helpful explanation and example! Thanks!
--Stephen

[gravatar]
mikey 11:51 PM on 3 Sep 2014

Looks like
http://clover.slavic.pitt.edu/~repertorium/plectogram/keys/keys.html
is no more.

Add a comment:

name
email
Ignore this:
not displayed and no spam.
Leave this empty:
www
not searched.
 
Name and either email or www are required.
Don't put anything here:
Leave this empty:
URLs auto-link and some tags are allowed: <a><b><i><p><br><pre>.