问题
I have some xml with thousands of movie elements which can all have 1 or MORE director and writer elements, if a director or writer has the same name as another, as seen here, I add an @differentiator of birthyear, if they are NOT the same person
<mediaList>
<movie id="1603934" dateCreated="2014-08-11">
<title>Night at the Museum</title>
<director>Shawn Levy</director>
<LCSpecialTopics>Comedy</LCSpecialTopics>
<writer>Robert Ben Garant</writer>
<writer differentiator="1970">Thomas Lennon</writer>
<language>English</language>
<year>2006</year>
</movie>
<movie lastModified="2014-08-30" id="1123629" dateCreated="2014-08-04">
<title type="foreign" xml:lang="cmn">Qiúgǎng Wèishì</title>
<title>Warriors of Qiugang</title>
<director>Ruby Yang</director>
<LCSpecialTopics>Documentary films</LCSpecialTopics>
<writer differentiator="1951">Thomas Lennon</writer>
<language>Chinese</language>
<year>2010</year>
</movie>
</mediaList>
my current XSLT to transform this into a nice readable table of
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:functx="http://www.functx.com" version="2.0">
<xsl:output method="html" indent="yes"/>
<xsl:function name="functx:substring-after-last-match" as="xs:string"
xmlns:functx="http://www.functx.com">
<xsl:param name="arg" as="xs:string?"/>
<xsl:param name="regex" as="xs:string"/>
<xsl:sequence select="
replace($arg,concat('^.*',$regex),'')
"/>
</xsl:function>
<xsl:template match="mediaList">
<html>
<head>
<link rel="stylesheet" type="text/css" href="CssJs/tableMedia.css"/>
<title>Media Table</title>
</head>
<body>
<table>
<thead>
<tr>
<th>#</th>
<th>Person</th>
<th>Movies</th>
</tr>
</thead>
<tbody>
<xsl:for-each-group select="movie" group-by="writer | director">
<xsl:sort
select="functx:substring-after-last-match(current-grouping-key(),'\s')"/>
<xsl:sort select="current-grouping-key()"/>
<xsl:variable name="person" select="current-grouping-key()"/>
<tr>
<td>
<xsl:value-of select="position()"/>
</td>
<td>
<p>
<a id="{current-grouping-key()}">
<xsl:value-of select="$person"/>
</a>
</p>
</td>
<td>
<xsl:if test="current-group()[writer = $person]">
<h3>Writer</h3>
</xsl:if>
<xsl:apply-templates
select="current-group()[writer = $person]/title[not(@type)]"/>
<xsl:if test="current-group()[director = $person]">
<h3>Director</h3>
</xsl:if>
<xsl:apply-templates
select="current-group()[director = $person]/title[not(@type)]"
/>
</td>
</tr>
</xsl:for-each-group>
</tbody>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="title">
<p>
<xsl:value-of select="."/>
<xsl:apply-templates select="../title[@type]"/>
</p>
</xsl:template>
<xsl:template match="title[@type]">
<span>
<xsl:value-of select="concat(' (',.,')')"/>
</span>
</xsl:template>
</xsl:stylesheet>
The problem is that I can't access the @differentiator after the group-by, I receive an error of "Required item type of first operand of '/' is node(); supplied value has item type xs:anyAtomicType. The expression can succeed only if the supplied value is an empty sequence."
from reading similar problems, i think there must be a way to perform this IN the group-by using concat() but because each movie element may have MORE than 1 writer/director I can't simply do
group-bye="concat(writer[@differentiator], writer/@differentiator) | writer | director">
desired output (snippet)would look like:
<tr>
<td>Some Number</td>
<td>
<p><a id="Thomas Lennon1951">Thomas Lennon (1951)</a></p>
</td>
<td>
<h3>Writer</h3>
<p>Warriors of Qiugang<span> (Qiúgǎng Wèishì)</span></p>
</td>
</tr>
<tr>
<td>Some Number</td>
<td>
<p><a id="Thomas Lennon1970">Thomas Lennon (1970)</a></p>
</td>
<td>
<h3>Writer</h3>
<p>Night at the Museum</p>
</td>
</tr>
回答1:
Elaborating on the brief answer given by Martin. Concatenate the string value of writer
and a potential differentiator
attribute and hand it to for-each-group
as the "group-by" value.
Then, two variables $person
and $year
separate the two parts of the grouping key again. This only solves the problem for writer
elements, not for director
.
As an aside note, usually IDs cannot contain whitespace. If your id attribute is a "true" ID, you should strip any whitespace in the ID value.
Also, use exclude-prefixes="#all"
to prevent unused namespaces from appearing in the output HTML.
Stylesheet
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="#all"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:functx="http://www.functx.com" version="2.0">
<xsl:output method="html" indent="yes"/>
<xsl:function name="functx:substring-after-last-match" as="xs:string"
xmlns:functx="http://www.functx.com">
<xsl:param name="arg" as="xs:string?"/>
<xsl:param name="regex" as="xs:string"/>
<xsl:sequence select="
replace($arg,concat('^.*',$regex),'')
"/>
</xsl:function>
<xsl:template match="mediaList">
<html>
<head>
<link rel="stylesheet" type="text/css" href="CssJs/tableMedia.css"/>
<title>Media Table</title>
</head>
<body>
<table>
<thead>
<tr>
<th>#</th>
<th>Person</th>
<th>Movies</th>
</tr>
</thead>
<tbody>
<xsl:for-each-group select="movie" group-by="writer/concat(@differentiator, '_', .), director">
<xsl:sort
select="functx:substring-after-last-match(current-grouping-key(),'\s')"/>
<xsl:sort select="current-grouping-key()"/>
<xsl:variable name="person" select="if (contains(current-grouping-key(),'_')) then substring-after(current-grouping-key(),'_') else current-grouping-key()"/>
<xsl:variable name="year" select="if (contains(current-grouping-key(),'_')) then substring-before(current-grouping-key(),'_') else ''"/>
<tr>
<td>
<xsl:value-of select="position()"/>
</td>
<td>
<p>
<a id="{concat(replace($person,' ',''),$year)}">
<xsl:value-of select="if ($year) then concat($person,' (',$year,')') else $person"/>
</a>
</p>
</td>
<td>
<xsl:if test="current-group()[writer = $person]">
<h3>Writer</h3>
</xsl:if>
<xsl:apply-templates
select="current-group()[writer = $person]/title[not(@type)]"/>
<xsl:if test="current-group()[director = $person]">
<h3>Director</h3>
</xsl:if>
<xsl:apply-templates
select="current-group()[director = $person]/title[not(@type)]"
/>
</td>
</tr>
</xsl:for-each-group>
</tbody>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="title">
<p>
<xsl:value-of select="."/>
<xsl:apply-templates select="../title[@type]"/>
</p>
</xsl:template>
<xsl:template match="title[@type]">
<span>
<xsl:value-of select="concat(' (',.,')')"/>
</span>
</xsl:template>
</xsl:stylesheet>
XML Input
I used the following input, with more than one movie written by Thomas Lennon (1970).
<mediaList>
<movie id="1603934" dateCreated="2014-08-11">
<title>Night at the Museum</title>
<director>Shawn Levy</director>
<LCSpecialTopics>Comedy</LCSpecialTopics>
<writer>Robert Ben Garant</writer>
<writer differentiator="1970">Thomas Lennon</writer>
<language>English</language>
<year>2006</year>
</movie>
<movie id="1603934" dateCreated="2014-08-11">
<title>Second movie</title>
<director>Frodo Baggins</director>
<LCSpecialTopics>Comedy</LCSpecialTopics>
<writer differentiator="1970">Thomas Lennon</writer>
<language>English</language>
<year>2006</year>
</movie>
<movie lastModified="2014-08-30" id="1123629" dateCreated="2014-08-04">
<title type="foreign" xml:lang="cmn">Qiúgǎng Wèishì</title>
<title>Warriors of Qiugang</title>
<director>Ruby Yang</director>
<LCSpecialTopics>Documentary films</LCSpecialTopics>
<writer differentiator="1951">Thomas Lennon</writer>
<language>Chinese</language>
<year>2010</year>
</movie>
</mediaList>
XML Output
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="CssJs/tableMedia.css">
<title>Media Table</title>
</head>
<body>
<table>
<thead>
<tr>
<th>#</th>
<th>Person</th>
<th>Movies</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>
<p><a id="FrodoBaggins">Frodo Baggins</a></p>
</td>
<td>
<h3>Director</h3>
<p>Second movie</p>
</td>
</tr>
<tr>
<td>2</td>
<td>
<p><a id="RobertBenGarant">Robert Ben Garant</a></p>
</td>
<td>
<h3>Writer</h3>
<p>Night at the Museum</p>
</td>
</tr>
<tr>
<td>3</td>
<td>
<p><a id="ThomasLennon1951">Thomas Lennon (1951)</a></p>
</td>
<td>
<h3>Writer</h3>
<p>Warriors of Qiugang<span> (Qiúgǎng Wèishì)</span></p>
</td>
</tr>
<tr>
<td>4</td>
<td>
<p><a id="ThomasLennon1970">Thomas Lennon (1970)</a></p>
</td>
<td>
<h3>Writer</h3>
<p>Night at the Museum</p>
<p>Second movie</p>
</td>
</tr>
<tr>
<td>5</td>
<td>
<p><a id="ShawnLevy">Shawn Levy</a></p>
</td>
<td>
<h3>Director</h3>
<p>Night at the Museum</p>
</td>
</tr>
<tr>
<td>6</td>
<td>
<p><a id="RubyYang">Ruby Yang</a></p>
</td>
<td>
<h3>Director</h3>
<p>Warriors of Qiugang<span> (Qiúgǎng Wèishì)</span></p>
</td>
</tr>
</tbody>
</table>
</body>
</html>
EDIT As a response to your comment:
What IS that comma doing though in the group-by rather than a |?
That's an excellent follow-up question! The |
operator returns the union of two sets of nodes, with duplicates removed and in document order. Its operands must necessarily be sets of nodes. But the first part of your group-by
value is not a set of nodes, it's just a string (the concat()
function returns a string). On the other hand, the operands of the comma operator ,
can be any kind of sequences, also ones of type xs:string
.
In short, you have to use ,
rather than |
because of the kinds of operands those operators accept. It has no bearing on the grouping whatsoever. When all sequences that contribute to the grouping key are sets of nodes, the two operators can be used interchangeably, save for potential changes in ordering.
回答2:
You can use group-by="writer/concat(@differentiator, '+', .), director"
.
来源:https://stackoverflow.com/questions/25585987/access-attribute-from-within-current-grouping-key-xslt