<xsl:key name="language-index" match="defn" use="@language"/>
Notice that the match attribute we used was the simple element name defn. This tells the XSLT processor to match all <defn> elements at all levels of the document. Because of the structure of our document, we could have written match="/glossary/glentry/defn", as well. Although this XPath expression is more restrictive, it matches the same elements because all <defn> elements must appear inside <glentry> elements, which in turn appear inside the <glossary> element.
Next, we set up our stylesheet to determine what value of the language attribute we're searching for. We'll do this with a global <xsl:param> element:
<xsl:param name="targetLanguage"/>
Recall from our earlier discussion of the <xsl:param> element that any top-level <xsl:param> is a global parameter to the stylesheet and may be set or initialized from outside the stylesheet. The way to do this varies from one XSLT processor to another. Here's how it's done with Xalan. (The command should be on one line.)
java org.apache.xalan.xslt.Process -in moreterms.xml -xsl crossref2.xsl
-param targetLanguage it
If you use Michael Kay's Saxon processor, the syntax looks like this:
java com.icl.saxon.StyleSheet moreterms.xml crossref2.xsl targetLanguage=it
Now that we've defined our key() function and defined a parameter to specify which language we're looking for, we need to generate our output. Here's the modified template that generates the HTML <title> and <h1> tags:
<xsl:template match="glossary">
<html>
<head>
<title>
<xsl:text>Glossary Listing: </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[1]/preceding-sibling::term"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[last()]/preceding-sibling::term"/>
</title>
</head>
<body>
<h1>
<xsl:text>Glossary Listing: </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[1]/ancestor::glentry/term"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[last()]/ancestor::glentry/term"/>
</h1>
<xsl:for-each select="key('language-index', $targetLanguage)">
<xsl:apply-templates select="ancestor::glentry"/>
</xsl:for-each>
</body>
</html>
</xsl:template>
There are a couple of significant changes here. When we were using the id() function, it was easy to find the first and last terms in the document. Because we're now trying to list only the definitions that are written in a particular language, that won't work. Reading the XPath expressions in the <xsl:value-of> elements from left to right, we find the first and last <defn> elements returned by the key() function, then use the preceding-sibling axis to reference the <term> element that preceded it. We could also have written our XPath expressions using the ancestor axis:
<h1>
<xsl:text>Glossary Listing: </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[1]/ancestor::glentry/term"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[last()]/ancestor::glentry/term"/>
</h1>
Now that we've successfully generated the HTML <title> and <h1> elements, we need to process the actual definitions for the chosen language. To do this, we'll use the targetLanguage parameter. Here's how the rest of the template looks:
<xsl:for-each select="key('language-index', $targetLanguage)">
<xsl:apply-templates select="ancestor::glentry"/>
</xsl:for-each>
In this code, we've selected all the values from the language-index key that match the targetLanguage parameter. For each one, we use the ancestor axis to select the <glentry> element. We've already written the templates that process these elements correctly, so we can just reuse them.
The final change we make is to select only those <defn> elements whose language attributes match the targetLanguage parameter. We do this with a simple XPath expression:
<xsl:apply-templates select="defn[@language=$targetLanguage]"/>
Here's the complete stylesheet:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="language-index" match="defn" use="@language"/>
<xsl:param name="targetLanguage"/>
<xsl:template match="/">
<xsl:apply-templates select="glossary"/>
</xsl:template>
<xsl:template match="glossary">
<html>
<head>
<title>
<xsl:text>Glossary Listing: </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[1]/preceding-sibling::term"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[last()]/preceding-sibling::term"/>
</title>
</head>
<body>
<h1>
<xsl:text>Glossary Listing: </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[1]/ancestor::glentry/term"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="key('language-index',
$targetLanguage)[last()]/ancestor::glentry/term"/>
</h1>
<xsl:for-each select="key('language-index', $targetLanguage)">
<xsl:apply-templates select="ancestor::glentry"/>
</xsl:for-each>
</body>
</html>
</xsl:template>
<xsl:template match="glentry">
<p>
<b>
<a name="{term/@id}"/>
<xsl:value-of select="term"/>
<xsl:text>: </xsl:text>
</b>
<xsl:apply-templates select="defn[@language=$targetLanguage]"/>
</p>
</xsl:template>
<xsl:template match="defn">
<xsl:apply-templates
select="*|comment()|processing-instruction()|text()"/>
</xsl:template>
<xsl:template match="xref">
<a href="#{@refid}">
<xsl:choose>
<xsl:when test="id(@refid)/@xreftext">
<xsl:value-of select="id(@refid)/@xreftext"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="id(@refid)"/>
</xsl:otherwise>
</xsl:choose>
</a>
</xsl:template>
<xsl:template match="seealso">
<b>
<xsl:text>See also: </xsl:text>
</b>
<xsl:for-each select="id(@refids)">
<a href="#{@id}">
<xsl:choose>
<xsl:when test="@xreftext">
<xsl:value-of select="@xreftext"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</a>
<xsl:if test="not(position()=last())">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:text>. </xsl:text>
</xsl:template>
</xsl:stylesheet>
Given our sample document and a targetLanguage of en, we get these results:
<html>
<head>
<title>Glossary Listing: applet - wildcard character</title>
</head>
<body>
<h1>Glossary Listing: applet - wildcard character</h1>
<p>
<b><a name="applet"></a>applet: </b>
An application program,
written in the Java programming language, that can be
retrieved from a web server and executed by a web browser.
A reference to an applet appears in the markup for a web
page, in the same way that a reference to a graphics
file appears; a browser retrieves an applet in the same
way that it retrieves a graphics file.
For security reasons, an applet's access rights are limited
in two ways: the applet cannot access the file system of the
client upon which it is executing, and the applet's
communication across the network is limited to the server
from which it was downloaded.
Contrast with <a href="#servlet">servlet</a>.
...
Changing the targetLanguage to it, the results are now different:
<html>
<head>
<title>Glossary Listing: applet - servlet</title>
</head>
<body>
<h1>Glossary Listing: applet - servlet</h1>
<p>
<b><a name="applet"></a>applet: </b>
[Pretend this is an Italian definition of applet.]
</p>
<p>
<b><a name="DMZlong"></a>demilitarized
zone (DMZ): </b>
[Pretend this is an Italian definition of DMZ.]
</p>
<p>
<b><a name="servlet"></a>servlet: </b>
[Pretend this is an Italian definition of servlet.]
</p>
</body>
</html>
With this stylesheet, we have a way to create a useful subset of our glossary. Notice that we're still using our original technique of ID, IDREF, and IDREFS to process the <xref> and <seealso> elements. If you want, you could redefine the processing to use the key() function instead. Here's how you'd define a key() function to mimic our earlier use of ID and IDREF:
<xsl:template match="xref">
<a href="#{@refid}">
<xsl:choose>
<xsl:when test="key('term-ids', @refid)[1]/@xreftext">
<xsl:value-of select="key('term-ids', @refid)[1]/@xreftext"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="key('term-ids', @refid)[1]"/>
</xsl:otherwise>
</xsl:choose>
</a>
</xsl:template>
As an exercise for the reader, you can modify this stylesheet so that it lists only definitions that apply to a particular topic, or only terms that are acronyms.
5.2.3.5. Solution #4: Use an extension function
The final approach is to write an extension function that tokenizes the refids attribute and returns a node-set containing all id values we need to search for. Xalan ships with an extension that does just that. We invoke the extension function on the value of the refids attribute, then use a <xsl:for-each> element to process all items in the node-set. We'll cover extension functions in Chapter 8, "Extending XSLT", but for now, here's what the stylesheet looks like:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:java="http://xml.apache.org/xslt/java"
exclude-result-prefixes="java">
<xsl:output method="html" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="term-ids" match="term" use="@id"/>
<xsl:template match="/">
<xsl:apply-templates select="glossary"/>
</xsl:template>
<xsl:template match="glossary">
<html>
<head>
<title>
<xsl:text>Glossary Listing: </xsl:text>
<xsl:value-of select="glentry[1]/term"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="glentry[last()]/term"/>
</title>
</head>
<body>
<h1>
<xsl:text>Glossary Listing: </xsl:text>
<xsl:value-of select="glentry[1]/term"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="glentry[last()]/term"/>
</h1>
<xsl:apply-templates select="glentry"/>
</body>
</html>
</xsl:template>
<xsl:template match="glentry">
<p>
<b>
<a name="{term/@id}"/>
<xsl:value-of select="term"/>
<xsl:text>: </xsl:text>
</b>
<xsl:apply-templates select="defn"/>
</p>
</xsl:template>
<xsl:template match="defn">
<xsl:apply-templates
select="*|comment()|processing-instruction()|text()"/>
</xsl:template>
<xsl:template match="xref">
<a href="#{@refid}">
<xsl:choose>
<xsl:when test="key('term-ids', @refid)[1]/@xreftext">
<xsl:value-of select="key('term-ids', @refid)[1]/@xreftext"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="key('term-ids', @refid)[1]"/>
</xsl:otherwise>
</xsl:choose>
</a>
</xsl:template>
<xsl:template match="seealso">
<b>
<xsl:text>See also: </xsl:text>
</b>
<xsl:for-each
select="java:org.apache.xalan.lib.Extensions.tokenize(@refids)">
<a href="{key('term-ids', .)/@id}">
<xsl:choose>
<xsl:when test="key('term-ids', .)/@xreftext">
<xsl:value-of select="key('term-ids', .)/@xreftext"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="key('term-ids', .)"/>
</xsl:otherwise>
</xsl:choose>
</a>
<xsl:if test="not(position()=last())">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:text>.</xsl:text>
</xsl:template>
</xsl:stylesheet>
In this case, the tokenize function (defined in the Java class org.apache.xalan.lib.Extensions) takes a string as input, then converts the string into a node-set in which each token in the original string becomes a node.
Be aware that using extension functions limits the portability of your stylesheets. The extension function here does what we want, but we couldn't use this extension function with Saxon, XT, or the XSLT tools from Oracle or Microsoft. They may or may not supply similar functions, and if they do, you'll have to modify your stylesheet slightly to use them. If it's important to you that you be able to switch XSLT processors at some point in the future, using extensions will limit your ability to do that.