1. | How do keys work? |
| Michael Kay Consider
<book id="1">
<author>John</author>
<author>Jane</author>
<author>Mary</author>
</book>
<book id="2">
<author>John</author>
</book>
<book id="3">
<author>John</author>
<author>Jane</author>
</book>
<xsl:key name="k" match="book" use="author"/>
For book id=1, the value of the use attribute is a node-set containing
three author elements. Each element contributes one value to the key.
The value that it contributes is the string-value of the author element.
So the three key values for book id=1 are "John", "Jane", and "Mary".
So the value of key('k', 'Jane') is the set containing book id=1 and
book id=3.
Is that clearer? |
2. | Keys, a general view. |
| Wendell Piez Actually, recursion isn't the easiest solution. It's to "prewire" the
hierarchy you want with keys. It's easiest to think of the wiring from
bottom up. It sounds from your description like your lowest level is C,
which are directly inside their most-previous A (or possibly P, whichever
is more recent). This relation can be established with
<xsl:key name="Records-by-parent" match="Record[F1='C']"
use="generate-id(preceding-sibling::Record[F1='A' or
F1='P'][1])"/>
The way this key works is, if I give it the value of a string, it will
return for me all C records that "belong to" (are intended to be children
of) any P or A record whose generated unique ID equals my string.
Likewise,
<xsl:key name="Records-by-parent" match="Record[F1='A']"
use="generate-id(preceding-sibling::Record[F1='P'][1])"/>
gets me back A records from the ID of a P record.
I build my hierarchy by pulling out all my P records, then inside each P
record's element, getting all the A and C records that belong to it (using
the ID of the P record as the key to retrieve them), then inside the As,
getting all the C records that belong to them.
Sort of recursion inside out, if you like.
I hope between this synopsis and the FAQ (more code), you can see what you
have to do. If not, ask again. |
3. | How do I use keys |
| Michael Kay
> Being new to XSLT I'm a little unsure of exactly what the pattern in
> the 'use' attribute value in the xsl:key element is doing.
If you want to index all employees by their works number, you do
<xsl:key name="k" match="employee" use="works-number"/>
and then to find an employee with works number 517541 you do
key('k', '517541')
The use attribute can be any XPath expression, so if you want to index employees on the number of events they have in their promotion history, you can do
<xsl:key name="k" match="employee" use="count(promotion-history/event)"/>
and then
key('k', 3)
will find all employees with three such events. |
4. | Unusual key usage |
| J.Pietschmann There is a general rule for using keys.
"The value used for looking up stuff via a key should be
fiddled with the same way as in the key definition" Some examples:
Normalizing space: <xsl:key name="n-stuff" match="stuff"
use="normalize-space(.)"/>
use
select="key('n-stuff',normalize-space($val))"
Transforming characters: <xsl:key name="t-stuff" match="stuff"
use="translate(.,$up,$lo)"/>
use
select="key('t-stuff',translate($keyval,$up,$lo))"
A composite key value: <xsl:key name="l" match="locale"
use="concat(country,'_',language)"/>
use
select="key('l',concat($c,'_',$l))"
The rule is particularly important when using keys for Munchean
grouping, because the values for lookup come from the source
XML and cannot be supposed to be already in a proper form. Unfortunately, this rule is not in any of the FAQs, and even
if it were, those in need to be reminded would hardly look for
it. :-) |
5. | Keys across multilple input files |
| Jeni Tennison
I have
managed to put together something that will probably work, though obviously
I've simplified a few things rather than use the full complexity of what I
know of what you're trying to do. I'm sure you can fill in the gaps. There is a file named filelist.xml that holds the names of the
XML class files. something like: <?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="test.xsl"?>
<documents>
<doc href="test1.xml" />
<doc href="test2.xml" />
<doc href="test3.xml" />
</documents> I'm also going to assume that it's the 'input file' for when you run the
stylesheet. If it isn't, you can always refer to it using
document('filelist.xml'). On to the stylesheet. First, we set up a variable that holds the contents
of all those documents:
<xsl:variable name="documents" select="document(/documents/doc/@href)" /> Note that the way the document() function works, it actually goes and gets
*all* those documents, not just the first one. Don't ask me why because I
can't follow the definition of document(), but it works. Which is handy. In this, $documents is assigned to *the result of the document() function
being called on* the node list that holds the 'href' attributes of all the
'doc' elements that are children of the 'documents' document element in the
input XML. Next I define the key in the normal way: <xsl:key name="classes" match="class" use="@name" /> Now, for lack of a better thing to do, I'm going to work through these
classes one at a time according to the order the documents have been given
in and the order the classes have been given in within those documents.
You probably have some more sophisticated way of ordering your output.
Slot it in here. <xsl:template match="/">
<xsl:for-each select="$documents">
<xsl:apply-templates select="/classes/class" />
</xsl:for-each>
</xsl:template> For each of the classes, I'm going to have a bit of information about the
class, and then generate the hierarchy that you (used to) want. You
definitely have a more sophisticated output for each class. Slot it in here. <xsl:template match="class">
<h3><xsl:value-of select="@name" /></h3>
<xsl:apply-templates select="." mode="hierarchy" />
</xsl:template> And finally, the bit where we use the key() function to get the superclass
node to build the hierarchy. Note that we have to define a variable for
the name of the superclass outside the xsl:for-each. The key() function
works in exactly the same way as normal, but the xsl:for-each defines the
documents that the key is used within. You definitely have a more
sophisticated output for the formatting and linking of the hierarchy. Slot
it in here. <xsl:template match="class" mode="hierarchy">
<xsl:variable name="superclass" select="@superclass" />
<xsl:for-each select="$documents">
<xsl:apply-templates select="key('classes', $superclass)"
mode="hierarchy" />
</xsl:for-each>
+- <xsl:value-of select="@name" />
</xsl:template> |
6. | Multiple Keys example |
| DaveP
After a long struggle and assistance, I finally managed
to use the key idea of XSLT. This one uses an example
which produces multiple results.
XML file.
<?xml version="1.0"?>
<!DOCTYPE doc [
<!ELEMENT doc (a*,b*,c*)* >
<!ELEMENT a (#PCDATA)>
<!ATTLIST a id ID #REQUIRED
idref IDREFS #REQUIRED>
<!ELEMENT b (#PCDATA)>
<!ATTLIST b id ID #REQUIRED
idref IDREF #REQUIRED>
<!ELEMENT c (#PCDATA)>
<!ATTLIST c id ID #REQUIRED
idref IDREF #REQUIRED>
]>
<doc>
<a id="ida" idref="idb">Element A content</a>
<a id="ida1" idref="ida2">Element A1 content</a>
<a id="ida2" idref="idb3">Element A2 content</a>
<a id="ida3" idref="idc">Element A3 content</a>
<b id="idb" idref="ida">Element B content</b>
<b id="idb1" idref="ida1">Element B1 content</b>
<b id="idb2" idref="ida3">Element B content</b>
<b id="idb3" idref="idc1">element B3 content</b>
<c id="idc" idref="idb1">Element C content</c>
<c id="idc1" idref="idb2">Element C1 content</c>
<c id="idc2" idref="ida">element C2 content</c>
<c id="idc3" idref="idb">Element C3 content</c>
</doc>
<!--
id is called by
a b, c2
a1 b1
a2 a1
a3 b2
b a, c3
b1 c
b2 c1
b3 a2
c a3
c1 b3
c2 none
c3 none
-->
XSL File
<?xml version="1.0"?>
<!-- Purpose: (Apart from demonstrating use of keys :-0)
To search for all elements which have an idref attribute
pointing to the element I'm dealing with. Look at the
comments at the end of the XML file to see what it 'should'
be. Its effectively the inverse of the id() function.
-->
<!DOCTYPE xsl:stylesheet [
<!ENTITY sp "<xsl:text> </xsl:text>">
<!ENTITY dot "<xsl:text>.</xsl:text>">
<!ENTITY nbsp " ">
<!ENTITY nl "
"><!--new line-->
]>
<xsl:transform
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0"
>
<xsl:output
method="text"
encoding="utf-8"
indent="yes"/>
<!-- Create the key.
Give it some name.
Give it a pattern to search for - it searches
the whole document but you don't need to specify
a full path unless you want to.
Give it a pattern to use for comparison when using
the key() function. This specifies the idref
attribute which is a child of elements a or b or c.
-->
<xsl:key name="caller" match="a|b|c" use="@idref"/>
<!-- This form is better if you need to change the use
attribute for each element. It could be idref with element a,
forward-link with element b etc.
Remember that the use attribute is the content that is
matched on, relative to the 'match' attribute which
specifies the pattern to search for.
<xsl:key name="caller" match="a" use="@idref"/>
<xsl:key name="caller" match="b" use="@idref"/>
<xsl:key name="caller" match="c" use="@idref"/>
-->
<xsl:template match="/doc">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="a|b|c">
<!-- Excuse the layout, this to produce output that is clear.
Same version is below, in comments, with fuller explanation. -->
<xsl:variable name="here" select="@id"/>
<xsl:value-of select="@id"/> is called by (<
xsl:choose><xsl:when test="(key('caller',$here))/@id"><
xsl:for-each
select="key('caller',$here)/@id"> <xsl:value-of
select="."/> <xsl:if
test="not(position()=last())">,&sp;<
/xsl:if></xsl:for-each></xsl:when>
<xsl:otherwise> None </xsl:otherwise></xsl:choose>)
&sp;&sp;&sp; <xsl:apply-templates/>
</xsl:template>
</xsl:transform>
<!--
Note that the processing is the same for all elements a b
and c
Use a local variable to hold the pattern I want to search
for. Why? Because the context node changes within the
for-each!
<xsl:variable name="here" select="@id"/>
Standard output, show which ID value I'm dealing with.
<xsl:value-of select="@id"/> is called by (
The choose identifies any elements which don't have any
other elements pointing at them, the when clause deals with
those that do, the otherwise clause deals with those that
don't. <xsl:choose>
The test is for an empty node-list. If its empty, then use
the otherwise clause. The key function returns a node-list,
which contains all the elements whose idref value
(use="@idref" in the key definition) matches the value of
$here, which is the current elements ID value. I only want
those elements which have an ID attribute, so when the key
function returns a node-list (singleton or first of many),
I use the result of that function with /@id appended.
<xsl:when test="(key('caller',$here))/@id">
Now, there may be more than one,
so iterate over the list, using for-each.
The select gives me each element with an id attribute
returned by the key.
<xsl:for-each select="key('caller',$here)/@id"> I
need the value of the current node (not, as I did earlier,
the value of the key function, note that the for-each has
done this for me. Big duh moment when I realised that :-)
<xsl:value-of select="."/> Now need to test if this
element from the node list is the last, and put out a comma
seperator if its not. <xsl:if
test="not(position()=last())">,&sp; </xsl:if>
</xsl:for-each> </xsl:when> Otherwise the node list
returned by the key function is empty say so.
<xsl:otherwise> None </xsl:otherwise>
</xsl:choose>) Space the output, to make it look pretty
&sp;&sp;&sp;&nl; And apply
templates. <grin> See what happens when you remove
it</grin> <xsl:apply-templates/>
-->
Output;
ida is called by (idb, idc2)
Element A content
ida1 is called by (idb1)
Element A1 content
ida2 is called by (ida1)
Element A2 content
ida3 is called by (idb2)
Element A3 content
idb is called by (ida, idc3)
Element B content
idb1 is called by (idc)
Element B1 content
idb2 is called by (idc1)
Element B content
idb3 is called by (ida2)
element B3 content
idc is called by (ida3)
Element C content
idc1 is called by (idb3)
Element C1 content
idc2 is called by ( None )
element C2 content
idc3 is called by ( None )
Element C3 content
|
7. | Keys >I would like to extract all paragraphs that have an attribute
>of doc equal to contract.
|
| Jeni Tennison
OK. First let's have a look at your input: <Template>
<Destination>
<Target doc="contract"/>
<Para>CONTRACT para destined for output to contract.xml</Para>
</Destination>
</Template>
.... other para's all with different doc value according to DTD
Note here that the 'Target' element is an empty one: it does not contain
the paragraph that it refers to. This contradicts your DTD, which says
that the 'Target' element should *contain* a 'Content' element rather than
be *followed by* a 'Para' element. I'll assume that your sample XML is
what you want, rather than the DTD.
Now have a look at your key:
<xsl:key name="blueprint" match="Target" use="@doc"/> Here you are making a key in which the 'Target' elements are indexed
according to their 'doc' attribute. When you use it later:
<xsl:template match="Destination">
<xsl:element name="{name()}">
<xsl:copy-of select="key('blueprint', '$contractType')"/>
</xsl:element>
</xsl:template>
you want to get the *Para* elements that follow that particular 'Target'
element, but instead are getting the 'Target' elements themselves (or would
be if you didn't quote the $contractType variable).
So, I'd change the way you define and use the key. Keys should generally
match the elements that you're actually interested in, which in this case
is the 'Para' elements. You can use anything reachable by an XPath
expression from that element as a key, so:
<xsl:key name="blueprint" match="Para" use="../Target/@doc" />
and
<xsl:template match="Destination">
<xsl:element name="{name()}">
<xsl:copy-of select="key('blueprint', $contractType)"/>
</xsl:element>
</xsl:template> Note that this will only get one paragraph - if there are multiple
paragraphs with the same value for the 'doc' attribute, then you should
iterate over them using xsl:for-each.
|
8. | Using keys |
| Jeni Tennison
>My aim is to extract paragraphs from the source tree
>(temp.xml) to be output to a result tree according to
>two criteria:
>
> * Target doc="contract" and
> * Target host="true"
>
>This is an extension of my original question.
>The output I receive is all paragraphs disregardless
>of attribute doc and host values. I have attempted
>to achieve my goal using two methods.
>ATTEMPT 1: Using the Key() function
>=============================
>
>I realise that only one key value can be used and
>then the other key value can be used on the filtered
>result - unless the intersection saxon extension
>function is used.
That's true, but you can make keys that combine two values, using concat().
So, we can index on a string like 'contract-true' using:
<xsl:key name="blueprint" match="Para"
use="concat(ancestor::Target/@doc, '-', ancestor::Target/@host)" />
Note that now that you've changed the way your input is structured, what
you're after are the 'doc' and 'host' attributes of the Target ancestors of
the particular Para, not a sibling.
>However here I still receive
>all paragraphs, it seems without referencing
>the index created by my xsl:key?
Have a look at your template:
><xsl:template match="Para">
> <xsl:element name="{name()}">
> <xsl:copy-of select="key('blueprint', contract)"/>
> <xsl:apply-templates/>
> </xsl:element>
></xsl:template>
You're matching all 'Para' elements, and then (for all of them), creating
an element called 'Para'. Within that element, you're putting a copy of
the node set that you get when you retrieve the nodes from the 'blueprint'
key, using the value of the 'contract' element as the key.
This is far from what you want to do. What you want to do is select all
the Para elements that are in the list that is retrieved when you use the
key 'contract-true' on the 'blueprint' key, and process only them, making a
copy of them. When you only want to process a subset of nodes, then you
should only apply templates to that subset, which means changing your
root-node matching template:
<xsl:template match="/">
<xsl:apply-templates select="key('blueprint', 'contract-true')" />
</xsl:template>
<xsl:template match="Para">
<xsl:copy-of select="." />
</xsl:template>
I'm not sure what exactly you want your output to look like: this just
copies the relevant 'Para' elements: you might want to wrap them in another
element, in which case you should put it in the root-node matching template.
>I have also, since simply typed the value of contract
>in the key, quotes do not seem to make any difference
>either way.
They do make a difference in how it's interpreted, just not in your overall
result because either way it wasn't working! :) When you put 'contract' in
quotes, it interprets it as the string 'contract', which is what you want.
When you don't put it in quotes, it interprets it as an XPath expression,
and tries to find a child element of the current node that is called
'contract', and use its content as the value to index into the key.
>ATTEMPT 2: Using a template match
>============================
>
>*** the following template is never used ***
>*** and I did hope that this template would ***
>*** select paragraphs according to attrib values ***
>*** specified? ***
>
><xsl:template match='Para/Target[@doc="contract"
> and @host="true"]'>
> <xsl:element name="{name()}">
> <xsl:copy-of select="attribute::node()"/>
> <xsl:apply-templates/>
> </xsl:element>
></xsl:template>
You need to change the XPath expression that you're using here. Let me
explain what the XPath you're using says:
Para/Target[@doc="contract" and @host="true"] Find an element called 'Target' that
has a parent element called 'Para' and
has a 'doc' attribute with a value of 'contract' and
has a 'host' attribute with a value of 'true'
There are no such Paras in your input.
What you want to say is:
Find an element called 'Para' that
has an ancestor element called 'Target' that
has a 'doc' attribute with a value of 'contract' and
has an ancestor element called 'Target' that
has a 'host' attribute with a value of 'true'
This can be expressed as:
Para[ancestor::Target[@doc="contract"] and
ancestor::Target[@host="true"]]
or
Target[@doc="contract"]//Para[ancestor::Target[@host="true"]]
or
Target[@host="true"]//Para[ancestor::Target[@doc="contract"]]
Given your current input, a better version (because it involves less
hunting around for matching nodes) is:
Find an element called 'Para' that
has a parent element called 'Target' that
has a 'host' attribute with a value of 'true' and
has a grandparent element called 'Target' that
has a 'doc' attribute with a value of 'contract'
This can be expressed as:
Target[@doc="contract"]/Target[@host="true"]/Para
You should also make sure that you *only* process those 'Para' elements,
because the XSLT Processor has built-in templates that match other elements
and will output their textual content. Again, this means putting your
XPath expression higher up in your templates: make sure that you then adapt
it according to the current node within the template you put it in. For
example:
<xsl:template match="/">
<xsl:apply-templates
select="//Para[ancestor::Target[@doc='contract'] and
ancestor::Target[@host='true']]" />
</xsl:template>
or
<xsl:template match="Template">
<xsl:apply-templates
select="Target[@doc="contract"]/Target[@host="true"]/Para" />
</xsl:template>
|
9. | Understanding Keys |
| Jeni Tennison
I've got trouble understanding the proper use of xsl:key and the
key() function. Here's my XML-File: <booklist>
<book>
<title>
<name>Design Patterns</name>
</title>
<author>Erich Gamma</author>
<author>Richard Helm</author>
<author>Ralph Johnson</author>
<author>John Vlissides</author>
</book>
<book>
<title>
<name>Pattern Hatching</name>
</title>
<author>John Vlissides</author>
</book>
<book>
<title>
<name>Building Applications</name>
</title>
<author>Mohamed Fayad</author>
<author>Douglas C. Schmidt</author>
<author>Ralph Johnson</author>
</book>
</booklist>
Here's the XSL:
<xsl:key name="test" match="title" use="name"/>
<xsl:template match="booklist">
<booklist>
<xsl:apply-templates/>
</booklist>
</xsl:template>
<xsl:template match="book">
<xsl:if test="key('test','Pattern Hatching')">
<node>
<xsl:value-of select="."/>
<xsl:text> -EndOfKeyValue-</xsl:text>
</node>
</xsl:if>
</xsl:template>
Now, I'd expect the xsl:if to make sure only "name" elements that are
children of "title" and have the content "Pattern Hatching" are
shown. Instead, I get ALL contents, including the author names; only
the various "book" contents are (naturally) placed into single
"node" elements.
Now, to make a long question short: WHY??? :)
When you define the key:
> <xsl:key name="test" match="title" use="name"/> Then you are setting up a number of associations between 'title' nodes and
key values given to them through their 'name' child:
node key value
title[1] 'Design Patterns'
title[2] 'Pattern Hatching'
title[3] 'Building Applications'
When you use: key('test', 'Pattern Hatching') you are saying "What nodes within the 'test' key have the key value of
'Pattern Hatching'?"
The answer is always the second title (in your example), so that node is
always returned as a result.
When you test on that node as in:
<xsl:if test="key('test', 'Pattern Hatching')">
...
</xsl:if>
then the test expression is evaluated and then converted to a boolean.
When node sets are converted to a boolean value, they return true() if
there is a node in the node set, and false() if not. In your case, since
the key() does return a node, the test is always true, so the contents of
xsl:if are always used, and you get information about all the books.
>Now, I'd expect the xsl:if to make sure only "name" elements that are
>children of "title" and have the content "Pattern Hatching" are
>shown.
If you are matching all books, as you are here, then it's easy to test
whether a book that you have has a 'title' child with a 'name' child that
has a content of 'Pattern Hatching'. You don't need key() to do it:
<xsl:if test="title/name = 'Pattern Hatching'">
...
</xsl:if>
Keys are really useful for *selecting* nodes that you want to process. So
if you only wanted to process the book with the title 'Pattern Hatching',
then you could use something like:
<xsl:template match="booklist">
<booklist>
<xsl:apply-templates
select="key('test', 'Pattern Hatching')" />
</booklist>
</xsl:template>
<xsl:template match="title">
<node>
<xsl:value-of select="name" />
</node>
</xsl:template>
If you want to give any information about the book aside from its title,
you also may be better off changing your key so that it matches on *books*
rather than on their *titles*. The match expression for a key should match
the nodes that you're actually interested in, rather than one of their
children, or an attribute. It's the 'use' expression that lets you select
*what* is interesting about the node (e.g. the name child of the title child):
<xsl:key name="test" match="book" use="title/name" /> and then:
<xsl:template match="booklist">
<booklist>
<xsl:apply-templates select="key('test', 'Pattern Hatching')" />
</booklist>
</xsl:template>
<xsl:template match="book">
<node>
<xsl:value-of select="title/name" />
</node>
</xsl:template>
|
10. | Keys. Some more explanation |
| Jeni Tennison
>> <xsl:key name="test" match="title" use="substring-before(name, ' ')"/>
>> will create
>> node key value
>> title[1] 'Design'
>> title[2] 'Pattern'
>> title[3] 'Building'
>>
>> I assume though I haven't tried it.
>
>I have done something similar, which was working fine. But I wanted
>to know how to do it with the key() function, and that's what I don't
>get working. The key function is just a simple look up - you give it a key value, it
tells you the nodes that are associated with that value. There isn't any
functionality in keys that allow you to look at all the key values and find
those that fulfill a certain pattern.
In your 'Pattern' example, say, to get the functionality that you want, the
processor would have to go through every single key value in the key, check
whether it contained the string 'Pattern', and then return those nodes that
have a key value that fulfill that test. This would probably be a lot more
complicated for the processor, certainly take a longer time.
Which isn't to say that it wouldn't be useful. In particular, we have to
jump through quite a lot of hoops to find out what the unique key values
are when doing grouping - a function that gave a list of all the key values
would be really helpful in doing this quickly.
If you know that the only titles that you're going to be interested in are
those containing the word 'Pattern', then you can hard code this into your
stylesheet with:
<xsl:key name="pattern-titles" match="title" use="contains(., 'Pattern')" />
This will give you a key that has two possible key values, 'true' and
'false'. If you then do key('pattern-titles', 'true'), then you will get a
list of those titles that contain 'Pattern'.
Obviously this technique is useless if the search string you're interested
in changes each time you run the stylesheet.
The other thing that you can do (my thanks to Mike Kay's book for pointing
this possibility out) is assign each title multiple key values within the
same key space. So you can do:
<xsl:key name="test" match="title" use="." />
<xsl:key name="test" match="title" use="substring-before(., ' ')" />
<xsl:key name="test" match="title" use="substring-after(., ' ')" />
If all your titles are less than two words long, then this will allow you
to do:
key('test', 'Pattern') and get all the 'title' elements whose title is 'Pattern' or whose first
word is 'Pattern' or whose last word (in a two-word title) is 'Pattern'.
You can add extra keys to your heart's content in order to break down the
title further:
<xsl:key name="test" match="title"
use="substring-before(substring-after(., ' '), ' '))" />
<xsl:key name="test" match="title"
use="substring-before(substring-after(substring-after(., ' '), '
'), ' ')" />
...
but it is a bit laborious! :)
|
11. | Restricting keys to less than the entire document |
| David Carlisle
Given
<?xml version="1.0"?>
<doc>
<itemlist>
<item itemid="Z101" units="1"/>
<item itemid="Z102" units="2"/>
<item itemid="Z101" units="4"/>
</itemlist>
<itemlist>
<item itemid="Z101" units="1"/>
<item itemid="Z10x2" units="2"/>
<item itemid="Z10x1" units="4"/>
</itemlist>
</doc>
The request was to sum items with identical itemid attributes
e.g. Z101 gives 5, not 6.
DC provides us with <?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/|doc">**
<xsl:apply-templates/>
</xsl:template>
<xsl:key name="items-by-itemid" match="item" use="concat(generate-id(..),@itemid)"
/>
<xsl:template match="itemlist">
<xsl:variable name="x" select="generate-id(.)"/>
<xsl:for-each select
="item[count(. | key('items-by-itemid', concat($x,@itemid))[1]) =
1]">
<xsl:sort select="@itemid" />
<tr>
<td><xsl:value-of select="@itemid"/></td>
<td><xsl:value-of
select="sum(key('items-by-itemid',concat($x,@itemid))/@units)"/></td>
</tr>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Jeni Tennison expands
However, if speed is more important and the itemlists are long, then
the vital thing that you need to do is narrow down the list of items
that the key gives you for a particular itemid in the context of a
particular itemlist.
There are two general ways of doing this. The first is, as David
Carlisle suggested, to include information about the itemlist that
you're currently looking at in the key value that you use to retrieve
the items you're interested in. At present, the key that you have
looks like:
<xsl:key name="items" match="item" use="@itemid" />
The key value is the value of the itemid attribute on the particular
item. You could include in that key value something about the
itemlist that the item belongs to: its id, if it has one, or a name,
or anything at all that distinguishes it from the other itemlists in
the document. The most general thing you can use is its unique id as
generated through the generate-id() function. You can include that in
your key value by concatenating it with the @itemid value:
<xsl:key name="itemlist-items" match="item"
use="concat(generate-id(parent::itemlist), '::', @itemid)" />
Then, in order to get the uniquely itemid'd items within a particular
itemlist, you can use the XPath:
item[generate-id() =
generate-id(key('itemlist-items',
concat(generate-id(parent::itemlist), '::', @itemid)))]
In other words, get the items whose unique id is the same as the
unique id of the (first) item returned from the 'items' key with a key
value equal to a concatenation of the unique id of the item's parent
itemlist, '::', and the item's itemid attribute.
You can also use:
item[1 = count(. |
key('itemlist-items',
concat(generate-id(parent::itemlist),
'::', @itemid))[1])]
In other words, get the items such that there is only one node in the
node set resulting from the union of that item and the first item
returned from the 'items' key with a key value equal to a
concatenation of the unique id of the item's parent itemlist, '::',
and the item's itemid attribute.
The second general solution is to have the key() return *all* the
items (no matter what itemlist they belong to), but filter that list
to only those items that are in the itemlist for the item you're
looking at. To use this method you have to have a context in which it
is possible to get at the common node set that you have in mind. For
example, in your case, you need to be in a context where the current
node is the itemlist that you're interested in, such as within a
template matching that itemlist.
Mike Brown suggested one way of doing this using the Kaysian Method
for finding node set intersections, which is a general approach that
is essential when all you know is the node set filter that you're
interested in, not what all those nodes have in common. For example,
if you had a global variable that held the node set that you were
using as a filter, then Mike's approach would be perfect.
In your case, though, you know that the node set filter involves the
identity of the itemlist for the items. If you have the unique
identity for the current itemlist within a variable:
<xsl:variable name="itemlist-id" select="generate-id()" />
then you can filter the node set returned by the key by testing which
of them have a parent itemlist that has the same unique id as the
current itemlist: key('items', @itemid)[generate-id(parent::itemlist) =
$itemlist-id]
You can insert this filtered key result into either of the Muenchian
XPaths:
item[generate-id() =
generate-id(key('items', @itemid)[generate-id(parent::itemlist) =
$itemlist-id])]
or:
item[1 = count(. |
key('items', @itemid)[generate-id(parent::itemlist) =
$itemlist-id][1])]
Ken Holman explains why the :: seperators are necessary This is precisely the basis of a section of my instructor-led tutorial and
exercise to do sub-tree subsetting of the xsl:key facility, but with one
addition. I teach
use="concat(generate-id(subtree-root-expression),' ',value-expression)"
because of the remote (but possible) synthesis of
ambiguous use values. If the generated id of two nodes were "N1" and
"N12", and the corresponding value expressions were coincidentally "23" and
"3", then the values would be "N123" and "N123".
Since the generated id is always a name token, and the name token can never
have a space character, the space is an effective delimiter to guarantee
uniqueness. |
12. | Problem with keys |
| Michael Kay
The key() function looks for nodes that are in the same document as the
context node. Your xsl:for-each is changing the context node to be one in a
different document.
|
13. | Understanding Keys with generate-id |
| Jeni Tennison
When you have a key that uses the generated ID of the node that it
matches, the key value can only ever get that single node - none of
the rest of the use expression matters, aside from as a test on the
values that you use in them. In more detail, when you do: <xsl:key name="MyKey1" match="a/b"
use="concat(generate-id(),':',@x,':',@y)"/>
<xsl:template match="a/b">
<xsl:for-each select="key('MyKey1',concat(generate-id(),':','1:1'))">
MyKey1:@id=<xsl:value-of select="@id"/><br/>
</xsl:for-each>
...
</xsl:template> In the template you're looking at a particular b element. This b
element will be indexed in the key according to its generated ID, its
x attribute and its y attribute. So given the b element: <b id="1" x="1" y="1" />
the key will be something like:
randomID1:1:1 *Only* this particular b element will have this key because
generate-id() produces a unique ID for a node. Thus each key value
will only ever access one node.
When you select nodes using the key, the evaluation of the
generate-id() for the b element is going to be exactly the same as it
was for the key. So for the above b element, the call looks like: key('MyKey1', 'randomID1:1:1') The only node this could possibly retrieve is the one identified by
that particular unique ID (i.e. the b element above). It won't
retrieve anything, though, if the x attribute and y attributes are not
both equal to 1, as it cannot match any nodes.
So basically, you're either retrieving the node itself or nothing at
all - it comes down to a test of whether the node's x and y attributes
are both 1 or not. And that means that it's exactly equivalent to
doing: <xsl:template match="a/b">
<xsl:if test="@x = 1 and @y = 1">
MyKey1:@id=<xsl:value-of select="@id" /><br />
</xsl:if>
</xsl:template> Usually you use generate-id() in a use expression because you want to
restrict the nodes that you retrieve from a key to a subtree of the
document - you use the unique ID of the parent of the nodes that
you're indexing, or one of its ancestors, or feasibly a descendant -
something where *lots* of nodes could have the same node related to
them.
[BTW, if you've got id attributes, it's more efficient to use them
rather than generating unique IDs all the time.] |
14. | Is it possible to use one key element for dual conditions |
| Michael Kay
> I use two keys for following conditions:
>
> when FacilityID exists, use prodCode key
> <xsl:key name="prodCode" match="z:row"
> use="concat(@FacilityID,':',@ProductCode)"/>
>
> when no FacilityID exists, then use prodCode2 key
> <xsl:key name="prodCode2" match="z:row" use="@ProductCode" />
>
> The above key element is applied to get a set of unique node-set
> and the related information.
>
> I am wondering is it possible to combine these two keys into
> one key
Use two xsl:key declarations with the same name:
<xsl:key name="prodCode" match="z:row[@FacilityId]"
use="concat(@FacilityID,':',@ProductCode)"/>
<xsl:key name="prodCode" match="z:row[not(@FacilityId)]"
use="@ProductCode" />
|
15. | Keys example |
| Jeni Tennison >I want to write a stylesheet such that after parsing this XML file, it
>counts the number
>of those elements which have the same partNum and report that in some way,
This is a grouping problem: you want to group the parts together, and count
the number of items in each group. Grouping problems are currently (and
using basic XSLT without any processor extensions) best solved using the
Muenchian method, which involves defining a key that does the grouping
quickly for you. When you design a key to help you group things together, you have three
variables to set: * name - a name for the key, anything you like: 'parts' in this case | * match - the things that you want to group: elements with 'partNum'
attributes in this case | * use - the thing that defines the groups: the number of the part in this case |
So, the key that you want looks like: <xsl:key name="parts" match="*[@partNum]" use="@partNum" /> This element is a top-level element: it goes right underneath the
xsl:stylesheet element. Note that I have assumed within the 'match'
expression that different elements, with different names, might all have
'partNum' attributes, so as well as: <CPU partNum="1345" />
you might have:
<HDD partNum="5437" /> If the elements that you're interested in are *all* CPUs, then you could use: <xsl:key name="CPUs" match="CPU" use="@partNum" /> instead. Indeed, you could define several different keys for each of the
elements that have partNums, if you know those in advance.
Retrieving the groups involves using the key() function. The first
argument is the name of the key (so 'parts' in this case) and the second
argument is the value of the thing that was used to group the things you're
grouping, so a part number in this case, something like: You can dynamically decide what the part number (or even key name) is.
Getting a list of the part numbers is the slightly tricky bit. You need to
identify one of the group for each of the groups that you've defined in the
key. You do this by comparing a node (a 'CPU' element) with the first node
that you get when you use the key value for that node to index into the
key. So, if the first element that you get when you use the value '1345'
to index into your 'parts' key is the same as the current element, then you
know that it's the first one to appear in the list. To compare nodes, you
use the generate-id() function. So, a template matching on the parent of
the CPU elements should look something like: <xsl:template match="parts">
<table>
<tr><th>partNum</th><th>Qty</th></tr>
<xsl:apply-templates
select="*[@partNum and
generate-id(.)=generate-id(key('parts', @partNum))]" />
</table>
</xsl:template> This guarantees that the only parts that have templates applied to them are
those that occur first in the list of parts with that particular part
number. You can then have a template that matches on them and outputs the
rows of your table. To count the number of parts with that particular part
number, you count the number of elements that are retrieved when you use
that part number to index into the key that you've used to group your
elements: <xsl:template match="*[@partNum]">
<tr>
<!-- first column is the value of the partNum attribute -->
<td><xsl:value-of select="@partNum" /></td>
<!-- second column is the number of parts with that partNum -->
<td><xsl:value-of select="count(key('parts', @partNum))" /></td>
</tr>
</xsl:template> This is tested and works in SAXON. The important things to take out of
this example are how to use the key to group the elements that you're
interested in: <xsl:key name="parts" match="*[@partNum]" use="@partNum" /> how to retrieve them and count them: count(key('parts', @partNum)) and how to retrieve the unique elements so that you can identify what part
numbers are used in your file: *[@partNum and
generate-id(.)=generate-id(key('parts', @partNum))] >The difficulty that I have is the following: I have different XML files
>(with similar structure)
>in which, these partNum are different , and may even change in the future,
I'm not sure whether this means you want to summarise the content of all
these different files at the same time. If you do, you should take note
that the key() function only indexes nodes in the documents that you are
currently working on. You can set the current documents using the
document() function, but that's another question :)
From your XSLT, I'm guessing that your input XML looks like: <PROJECTS>
<PROROW>
<id>1</id><name>Customer 1</name><project_name>Project 1</project_name>
</PROROW>
<PROROW>
<id>2</id><name>Customer 1</name><project_name>Project 2</project_name>
</PROROW>
<PROROW>
<id>3</id><name>Customer 2</name><project_name>Project 1</project_name>
</PROROW>
</PROJECTS> You are trying to group the rows according to the customer name. One of
the ways of dealing with grouping problems like this is to use the
Muenchian method.
First, define a key to group the rows according to the customer. You
define a key using the xsl:key element, which has to go at the top level in
the stylesheet (right under xsl:stylesheet). xsl:key takes three attributes:
* name - the name of the key, anything you like, but usually the thing
you're grouping: 'rows' in this case
* match - the nodes that you want to match - in your case, the PROROW elements
* use - the key that you want to use to group the nodes expressed as an
XPath from the thing you're matching to the key that you want to use to
access those nodes - in your case, the value of the 'name' element child of
the PROROW
So, your xsl:key element should look something like: <xsl:key name="rows" match="PROROW" use="name" /> The key indexes each of the rows according to the customer. You can use
the key() function to get the list of rows relating to a customer, so
key('rows', 'Customer 1') gives you a list of the PROROW elements with ids
1 and 2, whereas key('rows', 'Customer 2') gives you the PROROW element
with id 3. This makes it easy to get to your rows once you know the name
of the customer.
Next you have to arrange it so that you can get a list of the customers, in
which each customer only appears once. You've grouped the rows within the
key, and you know each group relates to one particular customer - it's a
matter of identifying one row from each group - the first, say. The way
you do this is through an XPath expression that identifies all the rows
that appear first in their group: PROROW[generate-id(.) = generate-id(key('rows', name)[1])]
This says:
Find a PROROW element such that
A unique ID generated for that element is the same as
A unique ID generated for
The first node in the node list returned by
Indexing the 'rows' key on the value of
The 'name' child element of that element If you apply templates only to those PROROW elements, then you know that
you are applying templates to one PROROW per customer: <xsl:template match="PROJECTS">
<xsl:apply-templates
select="PROROW[generate-id(.) = generate-id(key('rows', name)[1])]" />
</xsl:template> Your PROROW-matching template should now output information about all the
rows relating to the customer named in that PROROW. You can access them
through the key() function as described earlier, and cycle through them
using xsl:for-each (or through calling xsl:apply-templates, if you want). <xsl:template match="PROROW">
<b><xsl:value-of select="name" /></b>
<ul>
<xsl:for-each select="key('rows', name)">
<li>
<a href="projects_results.xml?project={id}">
<xsl:value-of select="project_name" />
</a>
</li>
</xsl:for-each>
</ul>
</xsl:template> [Notes: I've changed your HTML elements into lower case to make them XHTML
compatible. The links go to the id of the *row* rather than of the project
- I don't know if this is what is desired. Tested and works in SAXON.]
So, you can index the structs based on the field/type/refs that they
have: <xsl:key name="structs" match="struct" use="field/type/ref" /> Now, if I do key('structs', 'A') then I'll get all the structs that
have a field/type/ref with a value of 'A'. More importantly if I do: key('structs', {'A', 'B', 'C'}) where {'A', 'B', 'C'} is a node set of nodes with values 'A', 'B' and
'C', then I'll get all the structs that have field/type/refs with
values of 'A', 'B' or 'C'.
So, if I do: key('structs', $nodes/name) then I'll get all those structs who have field/type/refs with that
name.
What this boils down to is: try adding the key definition to your
stylesheet: <xsl:key name="structs" match="struct" use="field/type/ref" /> Then, use the following to define your $nextnodes variable. <xsl:variable name="nextnodes"
select="key('structs', $nodes/name)
[not($processed/name=name) and
not(field/type/ref[not(. = $processed/name)])]" /> Another thought is rather than keeping track of which nodes *have*
been processed, you could keep track of which nodes *haven't* been
processed. <xsl:template match="structs">
<xsl:call-template name="process">
<xsl:with-param name="nodes" select="struct[not(field/type/ref)]"/>
<xsl:with-param name="todo" select="struct[field/type/ref]"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="process">
<xsl:param name="nodes"/>
<xsl:param name="todo"/>
<xsl:for-each select="$nodes">
<xsl:value-of select="name"/>
</xsl:for-each>
<xsl:if test="$todo">
<xsl:variable name="nextnodes"
select="$todo[not(field/type/ref[. = $todo/name])]"/>
<xsl:if test="$nextnodes">
<xsl:call-template name="process">
<xsl:with-param name="nodes" select="$nextnodes"/>
<xsl:with-param name="todo"
select="$todo[not(name = $nextnodes/name)]"/>
</xsl:call-template>
</xsl:if>
</xsl:if>
</xsl:template>
With this solution it might just be more efficient to define a key to
split the structs into those with field/type/refs and those without: <xsl:key name="referencing-structs"
match="struct"
use="boolean(field/type/ref)" />
<xsl:template match="structs">
<xsl:call-template name="process">
<xsl:with-param name="nodes"
select="key('referencing-structs', false())" />
<xsl:with-param name="todo"
select="key('referencing-structs', true())" />
</xsl:call-template>
</xsl:template>
|
16. | Keys, finding contents |
| W. Eliot Kimber This should have been obvious, but it took me a while to figure it out
and I know a lot of people are struggling with key-based processing:
There's no direct way to see what the entries in a table are (e.g., the
equivalent of Hastable.getKeySet() in Java). However, you can easily
replicate what the key table is doing and sanity check what's happening.
All you do is a for-each from the root that uses the same select rule as
the key.
For example, if your key declaration is: <xsl:key name="primary"
match="index.marker"
use="normalize-space(concat(primary/@sortas,
primary[not(@sortas)]))"/>
Then to see what is actually going into the table, just do this: <xsl:for-each select="//index.marker">
<xsl:message>key='<xsl:value-of
select="normalize-space(concat(primary/@sortas,
primary[not(@sortas)]))"/>'</xsl:message>
</xsl:for-each> This should print out the equivalent of all the entries in the table.
You can also capture the key value and do a lookup in the table to see,
for example, how many of that entry are in the table under that key. <xsl:for-each select="//index.marker">
<xsl:variable name="tempkey">
<xsl:value-of
select="normalize-space(concat(primary/@sortas,
primary[not(@sortas)]))"/>
<xsl:variable>
<xsl:message>key='<xsl:value-of select="$tempkey"/>'</xsl:message>
<xsl:message> <xsl:value-of select="count(key('primary', $tempkey))/>
items found
</xsl:message>
</xsl:for-each>
Jeni adds In some cases, the key declaration may actually
be adding several entries to the table for each matched node. For
example: <xsl:key name="keywords" match="article" use="keywords/keyword" />
If the use attribute evaluates to a node set, then you get one entry
per node in that node set (all pointing to the same matched article).
So this key indexes each article against each of its keywords.
Therefore in these cases, the xsl:for-each you need is: <xsl:for-each select="//article/keywords/keyword">
<xsl:message>key='<xsl:value-of select="." />'</xsl:message>
</xsl:for-each> |
17. | Keys for cross-references |
| Jeni Tennison
> I want to pass in a collection/@name and get a list of all the
> corresponding part elements (cross refence). Then I want to sort the
> data first by part/@type, then by part/@name.
Whenever you're dealing with cross references, the first thing you
should do is set up a key that indexes the things that you want to get
access to by the value through which you want to access them.
In this case, you want to access the part elements by their id
attribute: <xsl:key name="parts" match="part" use="@id" /> You can then get a part with a particular id through the key function.
For example, the following gets the part with id '3':
One of the useful features of the key function is that if you pass in
a node set as the second argument, the XSLT processor goes through
each of the nodes, using their string value to retrieve nodes from the
key, and then unions the results together. So for example, if you're
on a collection element then you can get all the parts referenced by
all the ref element children (through their refid attributes) with:
This is really powerful in your case because it immediately gives you
the list of the parts that you want to sort. Your code becomes a lot
simpler: <xsl:template match="/large-collection/collection">
<xsl:if test="@name = $MyGroup">
<xsl:for-each select="key('parts', ref/@refid)">
<xsl:sort select="@type" />
<xsl:sort select="@name" />
<xsl:value-of select="concat('type: ', @type,
' name: ', @name,
' id: ', @id, '
')" />
</xsl:for-each>
</xsl:if>
</xsl:template>
By the way, I'd usually only apply templates to the nodes that I was
interested in; in your example, you could apply templates directly to
the collection element whose name is equal to $MyGroup, from within a
template matching the large-collection element: <xsl:template match="large-collection">
<xsl:apply-templates select="collection[@name = $MyGroup]" />
</xsl:template>
Then you wouldn't need the xsl:if within the template matching
collection elements. |
18. | Keys... again |
| Stuart Brown & David Carlisle
> <xsl:key match="*" use="name(.)" name="all-nodes"/>
> ..
> ...
> ....
> .....
> ......
> <xsl:if test="count(.| key('all-nodes', name(.))[1]) = 1">
> It works fine, i however do not understand what goes on in the above
line..
The xsl:key statement has set up an index of all nodes (match="*"), indexed
by their name (use="name(.)").
If you then supply the key() function with a value it returns the relevant
nodes as a nodeset. So,
key('all-nodes','foo') would return a nodeset of all the foo nodes in the
document. To extend this,
key('all-nodes', name(.)) returns all the nodes in the document with the
same name as your current node.
The addition of the [1] predicate filters this down to the first node in the
nodeset, which must be the
first in document order. Thus, key('all-nodes', name(.))[1] says "give me
the first element in document
order with the same name as the current element". The count function says
"count the number of nodes
which match the condition of being EITHER my current node (.) OR the first
node with the same name in
the document (obtained by the key() function)". So the only possible
condition in which a value of 1 could
be returned for this is if the current node IS the first node of that name
in the document.
Much the same with the generate-id() version: you are using key() to obtain
what you know is the first node
of that name in the document, and then comparing it with the current node to
see if they are one and the
same.
Took me a while to get my head round this, too!
David Carlisle offers:
This is Steve Meunch's devious trick.
To see how it works it helps to lie down in a darkened rule and repeat
the secret XSLT chant until you are in the correct frame of mind. Then
just look at the code again and it all becomes clear.
Basically the problem is that you have a node "." and a set of nodes
returned by the key() function. What you want to know is if the node you
have is the first element in the set. so..
key('contacts-by-surname', surname)[1]
is the first element (in document order) in the set.
and you want to know if this is the same node as .
you can't do
. = key('contacts-by-surname', surname)[1]
because that will test the string values of the nodes, it does not
test whether the nodes are the same. (So for example all empty elements
would be equal if tested with = as they all have string value "")
However the set (. | key('contacts-by-surname', surname)[1]) is the union of the set set consisting of . and the set consisting of
the first element returned by key(). Either this union has 1 element
or two. If it has two it means the key() function has returned a node
other than the current one. |
19. | Using Keys to group by position |
| David Carlisle Asked as, change each newline to a div element
div.xml - source file
<page>
<bodytext>This is the <link url="zzz">link</link> xxx
This is another line
and a <link url="zzz">link2</link> 2nd
and a third</bodytext>
</page>
Stylesheet div.xsl
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
version="1.0">
<xsl:output indent="no"/>
<xsl:key name="x" match="bodytext/node()"
use="generate-id((..|preceding-sibling::text()
[contains(.,' ')][1])[last
()])"/>
<xsl:template match="page">
<html>
<head>
<title>testing...</title>
</head>
<xsl:apply-templates/>
</html>
</xsl:template>
<xsl:template match="link">
<a href="{@url}">
<xsl:apply-templates/>
</a>
</xsl:template>
<xsl:template match="bodytext">
<body>
<xsl:for-each select=".|text()[contains(.,' ')]">
<xsl:call-template name="d1">
<xsl:with-param name="s"
select="substring-after(self::text(),' ')"/>
</xsl:call-template>
</xsl:for-each>
</body>
</xsl:template>
<xsl:template name="d1">
<xsl:param name="s"/>
<xsl:text> </xsl:text>
<xsl:choose>
<xsl:when test="contains($s,' ')">
<div><xsl:value-of
select="substring-before($s,' ')"/></div>
<xsl:call-template name="d1">
<xsl:with-param name="s" select="substring-after($s,' ')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<div>
<xsl:value-of select="$s"/>
<xsl:apply-templates
select="key('x',generate-id(.))[position()<last()]"/>
<xsl:value-of
select="substring-before(key('x',generate-id(.))[last()],' ')"/>
</div>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
$ saxon div.xml div.xsl
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>testing...</title></head>
<body>
<div>This is the <a href="zzz">link</a> xxx</div>
<div> This is another line</div>
<div> and a <a href="zzz">link2</a> 2nd</div>
<div> and a third</div></body>
</html>
|
20. | Tabulation, using keys |
| Mukul Gandhi
> I have a set of elements
> (recovered by SQL), each containing four child
> elements: property, prvalue, user_id and acct_id.
>
> I want to display it as:
> +--------+------------------+
> |property1 - prvalue1 |
> +--------+------------------+
> |user_id1|acct_id1, acct_id2|
> +--------+------------------+
> |user_id2|acct_id1 ... |
> +--------+------------------+
>
We need to use composite keys as suggested by
J.Pietschmann
With this input data
<?xml version="1.0" encoding="UTF-8"?>
<recordset>
<record>
<property>property2</property>
<prvalue>prvalue2</prvalue>
<user_id>user_id4</user_id>
<acct_id>acct_id6</acct_id>
</record>
<record>
<property>property1</property>
<prvalue>prvalue1</prvalue>
<user_id>user_id1</user_id>
<acct_id>acct_id1</acct_id>
</record>
<record>
<property>property2</property>
<prvalue>prvalue2</prvalue>
<user_id>user_id3</user_id>
<acct_id>acct_id3</acct_id>
</record>
<record>
<property>property1</property>
<prvalue>prvalue1</prvalue>
<user_id>user_id1</user_id>
<acct_id>acct_id2</acct_id>
</record>
<record>
<property>property2</property>
<prvalue>prvalue2</prvalue>
<user_id>user_id4</user_id>
<acct_id>acct_id5</acct_id>
</record>
<record>
<property>property1</property>
<prvalue>prvalue1</prvalue>
<user_id>user_id2</user_id>
<acct_id>acct_id1</acct_id>
</record>
<record>
<property>property2</property>
<prvalue>prvalue2</prvalue>
<user_id>user_id3</user_id>
<acct_id>acct_id4</acct_id>
</record>
</recordset>
The complete XSL is
<?xml version="1.0" encoding="UTF-8"?>
<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:key name="x" match="recordset/record"
use="property"/>
<xsl:key name="y" match="recordset/record"
use="concat(property,':',user_id)"/>
<xsl:template match="recordset">
<html>
<head>
<title>Grouping</title>
</head>
<body>
<table border="1">
<xsl:apply-templates select="record"/>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="record">
<xsl:if test="generate-id(.) =
generate-id(key('x',property)[1])">
<xsl:for-each select="key('x',property)">
<xsl:if test="position() = 1">
<tr>
<td>
<xsl:value-of select="property"/>
</td>
<td>
- <xsl:value-of select="prvalue"/>
</td>
</tr>
</xsl:if>
<xsl:if test="generate-id(.)=generate-id(key('y',
concat(property,':',user_id))[1])">
<xsl:for-each
select="key('y',concat(property,':',user_id))">
<xsl:if test="position() = 1">
<tr>
<td>
<xsl:value-of select="user_id"/>
</td>
<td>
<xsl:for-each select="key('y',
concat(property,':',user_id))">
<xsl:value-of
select="acct_id"/>
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:for-each>
</td>
</tr>
</xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
|
21. | Using keys to lookup from current stylesheet |
| Justin Makeig
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:a="http://cde.berkeley.edu/docbook/constant/acronym"
exclude-result-prefixes="a">
<xsl:key name="AcronymKey" match="a:acronymItem" use="a:acronym"/>
<xsl:template name="AcronymnStandsFor">
<xsl:param name="acronym"/>
<!-- change context to current document so the key will work -->
<xsl:for-each select="document('')">
<xsl:value-of select="key('AcronymKey',$acronym)/a:standsFor"/>
</xsl:for-each>
</xsl:template>
<!-- acronym lookups -->
<a:acronymList>
<a:acronymItem>
<a:acronym>Ant</a:acronym>
<a:standsFor>Another Neat Tool</a:standsFor>
</a:acronymItem>
...
</a:acronymList>
</xsl:stylesheet>
|
22. | performance enhancement in stylesheets by using keys! |
| Roger L. Costello I did a crude test of how much keys can speed up a stylesheet. Here's my
results.
I have a stylesheet which processes a 50 x 50 grid (a Vineyard with 50
tracts, each tract having 50 lots). I have 400 pickers that move around on
the Vineyard. Processing the Vineyard and pickers requires accessing the
lots thousands of times.
Here's the basic form of my stylesheet without keys:
<xsl:for-each select="1 to 50">
<xsl:variable name="tract-num" select="."/>
<xsl:for-each select="1 to 50">
<xsl:variable name="lot-num" select="."/>
<xsl:variable name="current-lot"
select="/vineyard/lot[@tract-num eq
$tract-num][$lot-num eq $lot-num]"/>
... get all neighboring lots ...
... process current and neighboring lots ...
</xsl:for-each>
</xsl:for-each>
Time required to process using this approach: 1 minute, 35 seconds (95
seconds).
Here's the basic form of my stylesheet using keys:
<xsl:for-each select="1 to 50">
<xsl:variable name="tract-num" select="."/>
<xsl:variable name="tract">
<xsl:sequence select="/vineyard/key('tracts', $tract-num)"/>
</xsl:variable>
<xsl:for-each select="1 to 50">
<xsl:variable name="lot-num" select="."/>
<xsl:variable name="current-lot"
select="$tract/key('lots', $lot-num)"/>
... get all neighboring lots ...
... process current and neighboring lots ...
</xsl:for-each>
</xsl:for-each>
Time required to process using this approach: 27 seconds
Without using keys it took more than 3 times longer than when I used keys! |
23. | Lookup XML file |
| Michael Kay
> The reason for a lookup xml source is to act as a cross-reference to
> data in the main xml source. So, I need to reference all elements
> who's ID = 123 but my match is expressed as a code value "abc". The
> code "abc" maps to 123 in my lookup.
> So, I'm still scratching my head wondering how I might effectively put
> the external file to use with keys, but thinking it won't work, the
> context would strictly be of the main xml source.
lookup.xml
<lookup>
<entry code="abc" value="123"/>
<entry code="xyz" value="987"/>
</lookup>
main.xml
<data>
<reading code="abc"/>
</data> required output
<data>
<reading value="123"/>
</data>
stylesheet
<xsl:transform ....
<xsl:key name="k" match="entry" use="@code"/>
<xsl:template match="*">
<xsl:copy><xsl:copy-of
select="@*"/><xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="reading">
<reading>
<xsl:variable name="code" select="@code"/>
<xsl:for-each select="document('lookup.xml')">
<xsl:attribute name="value">
<xsl:value-of select="key('k', $code)/@value"/> </xsl:attribute>
</xsl:for-each>
</reading>
</xsl:template>
or in 2.0 <xsl:template match="reading">
<reading value="{key('k', @code,
document('lookup.xml'))}"/>
</xsl:template>
|
24. | Bibliographic references |
| Mike Kay
> I am trying to output the following bibliography references
> (<bibref...>) in following format.
>
> ***This is the output that I want:***
>
> Refer to References (1, 4, 5) for guidelines on performing
> precision testing.
>
> ***The source file looks like this:***
>
> <para>Refer to References (<bibref
> xref="bib98861831"/>,<bibref
> xref="bib98861816"/>,<bibref xref="bib988618273"/>) for
> guidelines on performing precision testing.</para>
>
> <bibliography><title>REFERENCES</title>
> <bibliomixed id="bib98861831">National
> Committee...<booktitle>Fundamentals...</booktitle>...</bibliomixed>
>
> <bibliomixed id="bib98861814">National Committee...</bibliomixed>
> <bibliomixed id="bib98861870">National Committee...</bibliomixed>
> <bibliomixed id="bib98861816">National Committee...</bibliomixed>
> <bibliomixed id="bib988618273">National Committee for
> Clinical...</bibliomixed> </bibliography>
>
In 2.0 I would be inclined to do: <xsl:key name="b" match="bibliomixed" use="@id"/>
<xsl:template match="bibref">
<xsl:number select="key('b', @xref)"/>
</xsl:template>
Mark Shellenberger offers an XSLT 1.0 solution.
<xsl:template match="bibliography"/>
<xsl:template match="bibref">
<xsl:variable name="xref" select="@xref"/>
<xsl:for-each select="//bibliography/bibliomixed">
<xsl:if test="@id = $xref">
<xsl:value-of select="count(preceding-sibling::bibliomixed)+1"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
DC elaborates with a more general approach Always in any cross referencing in xslt the trick is to generate
the number on the _referenced_ node, often just using <xsl:number/> does
the job, sometimes you need to use count() but whatever it is just code
it once.
Then to make a cross reference (whether the reference text is a number
or a section heading or short id etc) the method is the same _go to the
referenced node_ and generate the text. so in this case <xsl:key name="bib" match="bibliomixed" use="@id"/>
<!-- make a numbered listing at the end -->
<xsl:template match="bibliomixed">
[<xsl:number/>] <xsl:apply-templates/>
</xsl:template>
<!--make xref-->
<xsl:template match="xref">
see [<xsl:for-each
select="key('bib',@xref)"><xsl:number/></xsl:for-each>]
</xsl:template>
see xsl:number is evaluated in both cases on the bibliomixed element,so
it makes the same number on both cases. If instead of [3] you wanted
[National Committee...]or [NatCom2006] or any other format you'd use the
smae basic idea, go the the bibliomixed node then (instead of
xsl:number) generate the same text for both the xref and the listing. |
25. | Check id, idref pairs |
| Mike Kay
> > Our schema has numerous elements with "id" attributes (target
> > IDs)
<xsl:key name="kid" match="*[@id]" use="@id"/>
> > and several with "refID" attributes (reference IDs).
> > Unfortunately, these are not defined as ID and IDREF
> > respectively or I wouldn't be having this issue. I need to
> > compare all of the "refID" attributes in a document against
> > all of the "id" attributes and generate a report showing the
> > "refID" attributes that do not have a valid target.
<xsl:for-each select="//*[@refID][not(key('kid', @refID)])">
<if test="not(.)">
<xsl:message>No key</xsl:message>
</xsl:if>
</xsl:for-each> |
26. | How to select using multiple Keys |
| David Carlisle, Mike Kay
How do I use multiple key values?
>
> Declaration:
> <xsl:key name="keyname" match="subroot" use="ccc"/>
>
> During the usage, I want to specify multiple values:
>
> <xsl:variable name="keyname" select="key('keyname', '11' or
> '22')"/> ==> Here I want to use multiple values 11 and 22.
>
> For the following xml, result should be:
> aaaaa bbbbb ccccc ddddd ==> note that both values of ccc=
> '22' and ccc= '11'
>
>
> <?xml version="1.0" encoding="UTF-8"?>
> <root>
> <subroot id="11111">
> <ccc>11</ccc> ==> needs to be picked
> <ddd>2005-08-26</ddd>
> <eee>aaaaa</eee>
> </subroot>
> <subroot id="11111">
> <ccc>22</ccc> ==> needs to be picked
> <ddd>2005-08-26</ddd>
> <eee>bbbbb</eee>
> </subroot>
> <subroot id="11111">
> <ccc>11</ccc>
> <ddd>2005-08-26</ddd>
> <eee>ccccc</eee>
> </subroot>
> <subroot id="11111">
> <ccc>11</ccc>
> <ddd>2005-08-26</ddd>
> <eee>ddddd</eee>
> </subroot>
> <subroot id="11111">
> <ccc>33</ccc>
> <ddd>2005-08-26</ddd>
> <eee>eeeee</eee>
> </subroot>
> </root>
>
In XSLT 2.0, you can supply a sequence:
key('keyname', ('111', '222'))
DC offers
<xsl:variable name="keyname"
select="key('keyname', '22')|key('keyname', '11')"/> |
27. | Using keys as a map/hashtable/dictionary |
| Dimitre Novatchev
I'm having some problems with constructs like hash tables or
indexes, which are quick in other languages but presently I
am using some brute force with. E.G.
<xsl:template name="month-of">
<xsl:param name="mon"/>
<xsl:choose>
<xsl:when test="lower-case($mon) = 'jan'">
<xsl:value-of select="'01'"/>
</xsl:when>
<xsl:when test="lower-case($mon) = 'feb'">
<xsl:value-of select="'02'"/>
</xsl:when>
Whenever you need a "HashTable" or "Dictionary" or "Map", try to use keys:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:m="my:Months">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<m:Months>
<m name="jan" mid="1"/>
<m name="feb" mid="2"/>
<m name="mar" mid="3"/>
<m name="apr" mid="4"/>
<m name="may" mid="5"/>
<m name="jun" mid="6"/>
<m name="jul" mid="7"/>
<m name="aug" mid="8"/>
<m name="sep" mid="9"/>
<m name="oct" mid="10"/>
<m name="nov" mid="11"/>
<m name="dec" mid="12"/>
</m:Months>
<xsl:key name="kIdFromName" match="@mid"
use="../@name"/>
<xsl:variable name="vUpper" select=
"'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vLower" select=
"'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="m/text()">
<xsl:variable name="vCur" select="."/>
<xsl:for-each select="document('')">
<xsl:value-of select=
"key('kIdFromName',
translate($vCur,$vUpper,$vLower)
)"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when this XSLT 1.0 transformation ()as you indicated you are limited
to XSLT 1.0) is applied on the following document:
<t>
<m>Mar</m>
<m>Jun</m>
<m>Aug</m>
<m>Oct</m>
<m>May</m>
<m>Jan</m>
<m>Dec</m>
<m>Nov</m>
<m>Sep</m>
<m>Jul</m>
<m>Feb</m>
<m>Apr</m>
</t>
the wanted, correct result is produced:
<t>
<m>3</m>
<m>6</m>
<m>8</m>
<m>10</m>
<m>5</m>
<m>1</m>
<m>12</m>
<m>11</m>
<m>9</m>
<m>7</m>
<m>2</m>
<m>4</m>
</t>
|