facebook google plus twitter
Webucator's Free XSLT 1.0 and XPath 1.0 Tutorial

Lesson: Working with Keys

Welcome to our free XSLT 1.0 and XPath 1.0 tutorial. This tutorial is based on Webucator's XSLT 1.0 and XPath 1.0 Training course.

Lesson Goals

  • How to create keys.
  • To use keys for cross referencing.
  • To work with the generate-id() function.
  • To use keys for grouping.

Key Basics

XSLT keys provide a simple and efficient way of looking up values based on other values. First, we will take a look at the syntax of keys and then we will look at how they are used in practice.

<xsl:key/>

Keys are created with the <xsl:key> tag, which must be a child of <xsl:stylesheet/>. It has three required attributes shown in the table below.

xsl:key Attributes
Attribute Description
name Name of the key.
match Pattern to match.
use The part of the matched node that will serve as the look-up index.

The key() Function

The key() function is used to look up the node or nodes specified by the key. It takes two arguments: the name of the key and the index.

The example below shows how to create a key and then look up a value in the key.

Code Sample:

Keys/Demos/SimpleKey.xml
<?xml version="1.0"?>
<?xml-stylesheet href="SimpleKey.xsl" type="text/xsl"?>
<beatles>
	<beatle link="http://www.paulmccartney.com">
		<name>
			<firstname>Paul</firstname>
			<lastname>McCartney</lastname>
		</name>
		<role>Singer</role>
	</beatle>
	<beatle link="http://www.johnlennon.com">
		<name>
			<firstname>John</firstname>
			<lastname>Lennon</lastname>
		</name>
		<role>Singer</role>
	</beatle>
	<beatle link="http://www.georgeharrison.com">
		<name>
			<firstname>George</firstname>
			<lastname>Harrison</lastname>
		</name>
		<role>Guitarist</role>
	</beatle>
	<beatle link="http://www.ringostarr.com">
		<name>
			<firstname>Ringo</firstname>
			<lastname>Starr</lastname>
		</name>
		<role>Drummer</role>
	</beatle>
</beatles>

Code Sample:

Keys/Demos/SimpleKey.xsl
<?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" indent="yes"/>
	<xsl:key name="keyBeatle" match="beatle" use="name/firstname"/>
	<xsl:param name="FirstName" select="'John'"/>
	
	<xsl:template match="/">
		<Match>
			<xsl:value-of select="key('keyBeatle',$FirstName)/@link"/>
		</Match>
	</xsl:template>
	
</xsl:stylesheet>

In the example above, the key is created with this line:

<xsl:key name="keyBeatle" match="beatle" use="name/firstname"/>

Imagine that it creates a lookup table that looks something like this:

Index Node-set
Paul Paul McCartney beatle node
John John Lennon beatle node
George George beatle node
Ringo Ringo beatle node

The key() function is used to look up the node or nodes specified by the key. It takes two arguments: the name of the key and the index.

<xsl:value-of select="key('keyBeatle',$FirstName)/@link"/>

As $Firstname is set to 'John' in the xsl:param, this line above returns "http://www.johnlennon.com". As you can see, the key() function returns nodes in the XML document. We can use XPath to drill down further into these nodes. In the example above, we drill down to the link attribute of the beatle node returned by key().

Creating a Simple Key

Duration: 10 to 15 minutes.

In this exercise, you will create and use a simple key.

  1. Open Keys/Exercises/SimpleKey.xml and review the code. You will see that it contains a list of City elements. Part of the code is shown below these instructions.
  2. Open Keys/Exercises/SimpleKey.xsl for editing.
  3. Create a key that indexes the City nodes by their State attribute.
  4. Using the key, output a City element for each City in New York State. For example:
    <City>New York</City>
  5. To test your solution, transform Keys/Exercises/SimpleKey.xml against Keys/Exercises/SimpleKey.xsl.

Code Sample:

Keys/Exercises/SimpleKey.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="SimpleKey.xsl"?>
<Cities Source="http://www.city-data.com/top1.html">
	<City State="NY" Population="8084316">New York</City>
	<City State="CA" Population="3798981">Los Angeles</City>
	<City State="IL" Population="2886251">Chicago</City>
	<City State="TX" Population="2009834">Houston</City>
	<City State="PA" Population="1492231">Philadelphia</City>
	<City State="AZ" Population="1371960">Phoenix</City>
	<City State="CA" Population="1259532">San Diego</City>
	<City State="TX" Population="1211467">Dallas</City>
	<City State="TX" Population="1194222">San Antonio</City>
---- C O D E   O M I T T E D ----

</Cities>

Code Sample:

Keys/Exercises/SimpleKey.xsl
<?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" indent="yes"/>
	<!--Create a key that indexes the City nodes by their State attribute.-->
	<xsl:template match="/">
		<Cities>
			<!--Output a City element for each City in New York State.-->
		</Cities>
	</xsl:template>
</xsl:stylesheet>

Solution:

Keys/Solutions/SimpleKey.xsl
<?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" indent="yes"/>
	<xsl:key name="keyCity" match="City" use="@State"/>
	<xsl:param name="State" select="'NY'"/>
	<xsl:template match="/">
		<Cities>
			<xsl:for-each select="key('keyCity',$State)">
				<City><xsl:value-of select="."/></City>
			</xsl:for-each>
		</Cities>
	</xsl:template>
</xsl:stylesheet>

Improving Performance with Keys

The major benefit of using keys is that the XSLT processor creates an index of the matched nodes, which allows it to access those nodes from anywhere without searching through the whole node tree.

Cross References

Keys are commonly used for cross referencing. To illustrate, we'll first study an example of a transformation that doesn't use keys. Take a look at the following XML document, which contains a list of U.S. states and their abbreviations followed by a list of cities with their states and populations.

Code Sample:

Keys/Demos/CitiesWithoutKey.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="CitiesWithOutKey.xsl"?>
<CityStates>
	<States>
		<State Abbr="AL">Alabama</State>
		<State Abbr="AK">Alaska</State>
		<State Abbr="AZ">Arizona</State>
		<State Abbr="AR">Arkansas</State>
		<State Abbr="CA">California</State>
		<State Abbr="CO">Colorado</State>
		<State Abbr="CT">Connecticut</State>
		<State Abbr="DE">Delaware</State>
		<State Abbr="FL">Florida</State>
---- C O D E   O M I T T E D ----

	</States>
	<Cities Source="http://www.city-data.com/top1.html">
		<City State="NY" Population="8084316">New York</City>
		<City State="CA" Population="3798981">Los Angeles</City>
		<City State="IL" Population="2886251">Chicago</City>
		<City State="TX" Population="2009834">Houston</City>
		<City State="PA" Population="1492231">Philadelphia</City>
		<City State="AZ" Population="1371960">Phoenix</City>
		<City State="CA" Population="1259532">San Diego</City>
		<City State="TX" Population="1211467">Dallas</City>
		<City State="TX" Population="1194222">San Antonio</City>
---- C O D E   O M I T T E D ----

	</Cities>
</CityStates>

Our goal is to take this document and output an HTML page that looks like this:

To do this, we'll loop through the City elements outputting a list item for each one. But the City elements just contain the state abbreviation and we want to output the full state name. To do this, we'll need to look up the state name in the State elements. The following code sample shows how to do this without keys.

Code Sample:

Keys/Demos/CitiesWithoutKey.xsl
<?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" indent="yes"/>
	<xsl:template match="/">
		<html>
			<head>
				<title>Largest Cities in the United States</title>
			</head>
			<body>
			<h1>Largest Cities in the United States</h1>
			<ol>
				<xsl:for-each select="//City">
					<xsl:variable name="State" select="@State"/>
					<li>
						<xsl:value-of select="."/>
						<xsl:text> is in the state of </xsl:text>
						<xsl:value-of select="//States/State[@Abbr=$State]"/>
						<xsl:text> and has a population of </xsl:text>
						<xsl:value-of select="format-number(@Population,'#,###')"/>
						<xsl:text>.</xsl:text>
					</li>
				</xsl:for-each>
			</ol>
			</body>
		</html>
	</xsl:template>
</xsl:stylesheet>

As suggested above, we loop through the City elements outputting list items. With each iteration, the context node is a different City element, which contains the city's name and population, so we can output those directly. The City element also contains the state abbreviation. The trick is to use that to get the full name of the state. This XPath does the trick: //States/State[@Abbr=$State]. It goes back up to the root of the document and looks through the State elements for one that has an abbreviation matching the State attribute (held in a variable) of the City element.

Although this works, it is an intensive process. For each City, the XSLT processor has to make a trip through the node tree looking for a matching State.

The Key Way

Let's take a look at another code sample that uses keys to accomplish the same thing.

Code Sample:

Keys/Demos/CitiesWithKey.xsl
<?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" indent="yes"/>
	<xsl:key name="keyState" match="State" use="@Abbr"/>
	<xsl:template match="/">
		<html>
			<head>
				<title>Largest Cities in the United States</title>
			</head>
			<body>
			<h1>Largest Cities in the United States</h1>
			<ol>
				<xsl:for-each select="//City">
					<li>
						<xsl:value-of select="."/>
						<xsl:text> is in the state of </xsl:text>
						<xsl:value-of select="key('keyState',@State)"/>
						<xsl:text> and has a population of </xsl:text>
						<xsl:value-of select="format-number(@Population,'#,###')"/>
						<xsl:text>.</xsl:text>
					</li>
				</xsl:for-each>
			</ol>
			</body>
		</html>
	</xsl:template>
</xsl:stylesheet>

In the example above, the key is created with this line:

<xsl:key name="keyState" match="State" use="@Abbr"/>

Imagine that it creates a lookup table that looks something like this:

Index Node-set
AL Alabama State node
AK Alaska State node
AZ Arizona State node
AR Arkansas State node
CA California State node

The key() function is used to look up the nodes specified by the key.

<xsl:value-of select="key('keyState',@State)"/>

If the value of the State attribute in the current City element is found in the lookup table created by the keyState key, then that value is output.

Again, the major reason this is better than the first way is that the XSLT processor creates an index of the nodes, so that it does not have to search through the whole node tree looking for a match.

Improving Performance with Keys

Duration: 15 to 20 minutes.

In this exercise, you will make an XSLT more efficient by using keys.

  1. Open Keys/Exercises/Top500SongsKey.xml and review the code. You will see that it contains a list of Song elements. Part of the code is shown below these instructions.
  2. Transform Keys/Exercises/Top500SongsKey.xml against Keys/Exercises/SongsByArtist.xsl. You will see that, for each song, it outputs the number of songs that artist has in the top 500. Currently, this is done without using keys.
  3. Open Keys/Exercises/SongsByArtist.xsl for editing.
  4. Modify the code so that it uses keys to do the same thing as it currently does.
  5. To test your solution, transform Keys/Exercises/Top500SongsKey.xml against Keys/Exercises/SongsByArtist.xsl.

Code Sample:

Keys/Exercises/Top500SongsKey.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="SongsByArtist.xsl"?>
<Songs Source="http://classicrock.about.com/library/misc/ptop500.htm">
	<Song Artist="Led Zeppelin">Stairway To Heaven</Song>
	<Song Artist="Rolling Stones">Satisfaction</Song>
	<Song Artist="Derek And The Dominoes">Layla</Song>
	<Song Artist="Beatles">A Day In The Life</Song>
	<Song Artist="Who">Won't Get Fooled Again</Song>
	<Song Artist="Doors">Light My Fire</Song>
	<Song Artist="Pink Floyd">Comfortably Numb</Song>
	<Song Artist="Eagles">Hotel California</Song>
	<Song Artist="Bruce Springsteen">Born To Run</Song>
---- C O D E   O M I T T E D ----

</Songs>

Code Sample:

Keys/Exercises/SongsByArtist.xsl
<?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" indent="yes"/>
	<xsl:template match="/">
		<Artists>
			<xsl:for-each select="Songs/Song">
				<xsl:variable name="Artist" select="@Artist"/>
				<Artist>
					<xsl:value-of select="@Artist"/>
					<xsl:text> songs in the top 500: </xsl:text>
					<xsl:value-of select="count(//Song[@Artist=$Artist])"/>
				</Artist>
			</xsl:for-each>
		</Artists>
	</xsl:template>
</xsl:stylesheet>

Challenge

Modify the code so that duplicates are removed. Sort the results by number of songs in the top 500 in descending order.

Solution:

Keys/Solutions/SongsByArtist.xsl
<?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" indent="yes"/>
	<xsl:key name="keySongs" match="Song" use="@Artist"/>
	<xsl:template match="/">
		<Artists>
			<xsl:for-each select="Songs/Song">
				<Artist>
					<xsl:value-of select="@Artist"/>
					<xsl:text> songs in the top 500: </xsl:text>
					<xsl:value-of select="count(key('keySongs',@Artist))"/>
				</Artist>
			</xsl:for-each>
		</Artists>
	</xsl:template>
</xsl:stylesheet>

Challenge Solution:

Keys/Solutions/SongsByArtist-challenge.xsl
<?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" indent="yes"/>
	<xsl:key name="keySongs" match="Song" use="@Artist"/>
	<xsl:template match="/">
		<Artists>
			<xsl:for-each select="Songs/Song">
				<xsl:sort select="count(key('keySongs',@Artist))" 
					order="descending" data-type="number"/>
				<xsl:variable name="Artist" select="@Artist"/>
				<xsl:if test="not(preceding-sibling::Song[@Artist=$Artist])">
					<Artist>
						<xsl:value-of select="@Artist"/>
						<xsl:text> songs in the top 500: </xsl:text>
						<xsl:value-of select="count(key('keySongs',@Artist))"/>
					</Artist>
				</xsl:if>
			</xsl:for-each>
		</Artists>
	</xsl:template>
</xsl:stylesheet>

Grouping

It is often necessary to group elements based on certain attributes or subelements. Consider our XML document that contains the largest cities in America. We might want to output these cities by state as shown below.

Code Sample:

Keys/Demos/CityByStateOutput.html
<?xml version="1.0" encoding="UTF-8"?>
<html>
<head>
<title>Most Populated Cities by State</title>
</head>
<body>
<h2>1. California - 17 Cities</h2>
<ol>
<li>Los Angeles</li>
<li>San Diego</li>
<li>San Jose</li>
---- C O D E   O M I T T E D ----

</ol>
<h2>2. Texas - 12 Cities</h2>
<ol>
<li>Houston</li>
<li>Dallas</li>
<li>San Antonio</li>
---- C O D E   O M I T T E D ----

</ol>
<h2>3. Arizona - 5 Cities</h2>
<ol>
<li>Phoenix</li>
<li>Tucson</li>
<li>Mesa</li>
---- C O D E   O M I T T E D ----

</body>
</html>

In XSLT 1.0, there is no tag meant for grouping, but there are at least a couple of ways to get the job done. The most efficient way to do this is to use a method called the Muencian method after Steve Muench who created it. This method requires a good understanding of the generate-id() function discussed below.

The generate-id() Function

The generate-id() function creates ids that uniquely identify a node in an XML document. It takes a single argument: the node-set to identify. The value of the generated id is based on the first node in the node-set, and the same id will be generated no matter how the node is retrieved. To illustrate this, take a look at the following two files.

Code Sample:

Keys/Demos/GenerateId.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="GenerateID.xsl"?>
<Songs Source="http://classicrock.about.com/library/misc/ptop500.htm">
	<Song Artist="Led Zeppelin">Stairway To Heaven</Song>
	<Song Artist="Rolling Stones">Satisfaction</Song>
	<Song Artist="Derek And The Dominoes">Layla</Song>
---- C O D E   O M I T T E D ----

</Songs>

Code Sample:

Keys/Demos/GenerateId.xsl
<?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" indent="yes"/>
	<xsl:key name="keySong" match="Song" use="@Artist"/>
	<xsl:template match="/">
		<IDs>
			<ID>
				<xsl:value-of select="Songs/Song[2]"/>
				<xsl:text>: </xsl:text>
				<xsl:value-of select="generate-id(Songs/Song[2])"/>
			</ID>
			<ID>
				<xsl:value-of select="Songs/Song[.='Satisfaction']"/>
				<xsl:text>: </xsl:text>
				<xsl:value-of select="generate-id(Songs/Song[.='Satisfaction'])"/>
			</ID>
			<ID>
				<xsl:value-of select="Songs/Song[@Artist='Rolling Stones']"/>
				<xsl:text>: </xsl:text>
				<xsl:value-of 
					select="generate-id(Songs/Song[@Artist='Rolling Stones'])"/>
			</ID>
			<ID>
				<xsl:value-of select="key('keySong','Rolling Stones')"/>
				<xsl:text>: </xsl:text>
				<xsl:value-of select="generate-id(key('keySong','Rolling Stones'))"/>
			</ID>
			<ID>
				<xsl:value-of select="Songs/Song[starts-with(@Artist,'R')]"/>
				<xsl:text>: </xsl:text>
				<xsl:value-of 
					select="generate-id(Songs/Song[starts-with(@Artist,'R')])"/>
			</ID>
		</IDs>
	</xsl:template>
</xsl:stylesheet>

The generate-id() function is passed five different XPaths and returns the same output each time. Again, this is because the first element in the node-set is always the same.

Code Sample:

Keys/Demos/GenerateIdOutput.xml
<?xml version="1.0" encoding="UTF-8"?>
<IDs>
<ID>Satisfaction: IDAMAK4B</ID>
<ID>Satisfaction: IDAMAK4B</ID>
<ID>Satisfaction: IDAMAK4B</ID>
<ID>Satisfaction: IDAMAK4B</ID>
<ID>Satisfaction: IDAMAK4B</ID>
</IDs>

As shown below, we can use this knowledge to find the first City element with a specific State attribute.

Code Sample:

Keys/Demos/BiggestCitiesByState.xsl
<?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" indent="yes"/>
	<xsl:key name="keyCities" match="City" use="@State"/>
	
	<xsl:template match="/">
		<html>
			<head>
				<title>Most Populated Cities by State</title>
			</head>
			<body>
				<ol>
				<xsl:for-each 
					select="//City[generate-id(.)=generate-id(key('keyCities',@State))]">
					<!--
						The first generate-id() above returns an id for the current City.
						The second generate-id() above returns an id for the first 
							City with a State attribute that is the same as the current 
							City's State attribute.
						These values will only be equal for the first City elements 
							with a specific State attribute.
						So, this for-each loops through the each City first to specify
							a particular State.
					-->
					<li>
						<xsl:value-of select="."/>, <xsl:value-of select="@State"/>
					</li>
				</xsl:for-each>
				</ol>
			</body>
		</html>
	</xsl:template>
	
</xsl:stylesheet>

As indicated in the comment:

  1. The first generate-id() above returns an id for the current City.
  2. The second generate-id() above returns an id for the first City with a State attribute that is the same as the current City's State attribute.
  3. These values will only be equal for the first City elements with a specific State attribute. So, this for-each loops through each City first to specify a particular State.
  4. As the XML document lists the cities in descending order by population, the most populated cities in each state are returned.

Using generate-id for Grouping

Okay. Now let's get back to grouping. We've learned how to use generate-id() and keys pick just one city from every state. Another way to look at this is that we've learned how to retrieve each distinct state once and only once. To group by state, we need loop through the City elements for each state and return all the cities in that state. But we don't need to go through the whole node tree again, because we have a key that contains all City elements by State. Take a look at the following code.

Code Sample:

Keys/Demos/GroupCities.xsl
<?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" indent="yes"/>
	<xsl:key name="keyCities" match="City" use="@State"/>
	<xsl:key name="keyState" match="State" use="@Abbr"/>
	
	<xsl:template match="/">
		<html>
			<head>
				<title>Most Populated Cities by State</title>
			</head>
			<body>
				<xsl:for-each 
					select="//City[generate-id(.)=generate-id(key('keyCities',@State))]">
					<!--
						The first generate-id() above returns an id for the current City.
						The second generate-id() above returns an id for the first 
							City with a State attribute that is the same as the current 
							City's State attribute.
						These values will only be equal for the first City elements 
							with a specific State attribute.
						So, this for-each loops through the each City first to specify
							a particular State.
					-->
					<xsl:sort select="count(key('keyCities',@State))" 
						order="descending" data-type="number"/>
					<h2>
						<xsl:value-of select="position()"/>
						<xsl:text>. </xsl:text>
						<xsl:value-of select="key('keyState',@State)"/>
						<xsl:text> - </xsl:text>
						<xsl:value-of select="count(key('keyCities',@State))"/>
						<xsl:text> Cities</xsl:text>
					</h2>
					<ol>
						<xsl:for-each select="key('keyCities',@State)">
							<li><xsl:value-of select="."/></li>
						</xsl:for-each>
						<!--
							This nested for-each loops through the City elements that have
								the same value for the State attribute as the current City in
								the outer for-each loop.
						-->
					</ol>
				</xsl:for-each>
			</body>
		</html>
	</xsl:template>
	
</xsl:stylesheet>

Remember that the key() function returns a node-set. We loop through this node-set with the following code:

<xsl:for-each select="key('keyCities',@State)"> <li><xsl:value-of select="."/></li> </xsl:for-each>

We also sort the outer loop by state according to the number of cities per state. This is done by counting the number of City elements matched by the key() function:

<xsl:sort select="count(key('keyCities',@State))" order="descending" data-type="number"/>

Grouping Songs By Artist

Duration: 20 to 30 minutes.

In this exercise, you will practice grouping with keys and the generate-id() function.

  1. Open Keys/Exercises/Top500SongsGrouped.xml and review the code. You will see that it contains a list of Song elements. We've seen this document before.
  2. Open Keys/Exercises/GroupSongs.xsl for editing.
  3. Write code that outputs the songs by artist. For each artist, the output should appear something like this:
    <h2>34. Animals - 4 Song(s)</h2>
    <ol>
    	<li>House Of The Rising Sun</li>
    	<li>We Gotta Get Out Of This Place</li>
    	<li>Don't Let Me Be Misunderstood</li>
    	<li>It's My Life</li>
    </ol>
    where 34 is the position of the artist (i.e., there are 33 artists that appear before the Animals, Animals is the name of the artist, and 4 is the number of songs the artist has in the top 500.
  4. To test your solution, transform Keys/Exercises/Top500SongsGrouped.xml against Keys/Exercises/GroupSongs.xsl.

Code Sample:

Keys/Exercises/GroupSongs.xsl
<?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" indent="yes"/>
	<xsl:key name="keySong" match="Song" use="@Artist"/>
	
	<xsl:template match="/">
		<html>
			<head>
				<title>Hits By Artist</title>
			</head>
			<body>
				<!--
					Write code that outputs the songs by artist.
					Challenge: Sort the results so that the artist with the most 
						songs appears first.
				-->
			</body>
		</html>
	</xsl:template>
	
</xsl:stylesheet>

Challenge

Sort the results so that the artist with the most songs appears first.

Solution:

Keys/Solutions/GroupSongs.xsl
<?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" indent="yes"/>
	<xsl:key name="keySong" match="Song" use="@Artist"/>
	
	<xsl:template match="/">
		<html>
			<head>
				<title>Hits By Artist</title>
			</head>
			<body>
			<xsl:for-each 
				select="Songs/Song[generate-id(.)=generate-id(key('keySong',@Artist))]">
				<h2>
					<xsl:value-of select="position()"/>.  
					<xsl:value-of select="@Artist"/> -
					<xsl:value-of select="count(key('keySong',@Artist))"/> Song(s)
				</h2>
				<ol>
					<xsl:for-each select="key('keySong',@Artist)">
						<li><xsl:value-of select="."/></li>
					</xsl:for-each>
				</ol>
			</xsl:for-each>
			</body>
		</html>
	</xsl:template>
	
</xsl:stylesheet>

Challenge Solution:

Keys/Solutions/GroupSongs-challenge.xsl
<?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" indent="yes"/>
	<xsl:key name="keySong" match="Song" use="@Artist"/>
	
	<xsl:template match="/">
		<html>
			<head>
				<title>Hits By Artist</title>
			</head>
			<body>
			<xsl:for-each 
				select="Songs/Song[generate-id(.)=generate-id(key('keySong',@Artist))]">
				<xsl:sort select="count(key('keySong',@Artist))" 
					order="descending" data-type="number"/>
				<h2>
					<xsl:value-of select="position()"/>.  
					<xsl:value-of select="@Artist"/> -
					<xsl:value-of select="count(key('keySong',@Artist))"/> Song(s)
				</h2>
				<ol>
					<xsl:for-each select="key('keySong',@Artist)">
						<li><xsl:value-of select="."/></li>
					</xsl:for-each>
				</ol>
			</xsl:for-each>
			</body>
		</html>
	</xsl:template>
	
</xsl:stylesheet>