XSLT position() - exlude elements that fail test condition - xslt

lets say I have the following xml file:
<jobs>
<job>
<PositionTitle>Painter</PositionTitle>
<InternalOrExternal>External</InternalOrExternal>
</job>
<job>
<PositionTitle>Plumber</PositionTitle>
<InternalOrExternal>Internal</InternalOrExternal>
</job>
<job>
<PositionTitle>Chemist</PositionTitle>
<InternalOrExternal>Internal</InternalOrExternal>
</job>
<job>
<PositionTitle>Teacher</PositionTitle>
<InternalOrExternal>External</InternalOrExternal>
</job>
</jobs>
.
I process it with the following xslt to show only the external jobs:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:content="http://purl.org/rss/1.0/modules/content/" >
<xsl:output method="html"/>
<xsl:template match="/">
<xsl:for-each select="jobs/job">
<xsl:if test="InternalOrExternal = 'External'"><!-- Only show external jobs -->
<xsl:value-of select="PositionTitle"/> - <xsl:value-of select="position()"/><br />
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This results as:
Painter - 1
Teacher - 4
I assume the position() function returns the actual position in the xml file and doesn't take into account any if statements that may exclude some jobs. In this case the Internal jobs are excluded but their position is still counted.
The result that I want is:
Painter - 1
Teacher - 2
Is their any way I can get the position() function to only count what I display?
I've tried this with no luck:
<xsl:value-of select="position(jobs/job[InternalOrExternal='External'])"/>

position() gives you the position of the current node within the "current node list", i.e. whatever list of nodes were selected by the most recent for-each or apply-templates. You can think of this informally as the current iteration number of the for-each1. By saying
<xsl:for-each select="jobs/job">
the current node list is all the job elements, so you get position 1 for the first one and position 4 for the fourth one. If you remove the xsl:if and instead move the filtering into a predicate on the for-each selector:
<xsl:for-each select="jobs/job[InternalOrExternal='External']">
<xsl:value-of select="PositionTitle"/> - <xsl:value-of select="position()"/><br />
</xsl:for-each>
then the current node list only includes the "External" job elements, and you'll get the position() values you require.
1 Since XSLT instructions don't have side effects (e.g. there are no updateable variables), the XSLT processor doesn't necessarily have to implement for-each internally using a sequential loop. It could choose to process different nodes in parallel or out of order, as long as it assembles the output in such a way that it looks the same as a sequential loop would produce.

In addition to Ian Roberts' great answer: If you had to have the context be job (for whatever reason), you could also use xsl:number instead of position().
Example:
<xsl:value-of select="PositionTitle"/> - <xsl:number count="job[InternalOrExternal='External']"/><br />

Related

Counting previous nodes returns unexpected results

I'd like to use itemized list instead of hard breaks, see the example:
<para>line1<?linebreak?>
line2<?linebreak?>
line3</para>
However, I am experiencing weird behavior in my recursive template which prevents processing the second line correctly. I've created simplified test case - not recursive any more. If count(preceding::processing-instruction('linebreak')) = 0 expression is used this way, nothing is returned, but I would expect the second line.
<line>line1</line><node>
line2<?linebreak?>
line3</node>
line2
That <node> element is for debugging purposes here. It confirms I process expected data.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="para[processing-instruction('linebreak')]">
<xsl:call-template name="getLine">
<xsl:with-param name="node" select="./node()"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="getLine">
<xsl:param name="node"/>
<line>
<xsl:copy-of
select="$node/self::processing-instruction('linebreak')[not(preceding::processing-instruction('linebreak'))]/preceding::node()"
/>
</line>
<xsl:call-template name="getSecondLine">
<xsl:with-param name="node"
select="$node/self::processing-instruction('linebreak')[not(preceding::processing-instruction('linebreak'))]/following::node()"
/>
</xsl:call-template>
</xsl:template>
<xsl:template name="getSecondLine">
<xsl:param name="node"/>
<node>
<xsl:copy-of select="$node"/>
</node>
<xsl:copy-of
select="$node/self::processing-instruction('linebreak')[count(preceding::processing-instruction('linebreak')) = 0]/preceding::node()"
/>
</xsl:template>
</xsl:stylesheet>
Tested in Saxon HE/EE 9.6.0.7 (in Oxygen XML Editor 18).
The processing of the first linebreak works correctly:
<line>
<xsl:copy-of
select="$node/self::processing-instruction('linebreak')
[not(preceding::processing-instruction('linebreak'))]
/preceding::node()"/>
</line>
though only on this sample; on more complex data, you would get the wrong results because you should be using the preceding-sibling axis rather than the preceding axis.
But the code could be greatly simplified, I would write the select expression as:
select="$node[self::processing-instruction('linebreak')][1]
/preceding-sibling::node()"
The processing of the second linebreak seems very confused. You are passing the parameter
$node/self::processing-instruction('linebreak')
[not(preceding::processing-instruction('linebreak'))]
/following::node()"
which is effectively
select="$node[self::processing-instruction('linebreak')][1]
/following-sibling::node()"
which selects the three nodes
line2<?linebreak?>line3
(plus whitespace) which you are outputting within a <node> element, producing
<node>line2<?linebreak?>line3</node>
(again ignoring whitespace)
and then you do
select="$node/self::processing-instruction('linebreak')
[count(preceding::processing-instruction('linebreak'))=0]
/preceding::node()"
Here $node/self::processing-instruction('linebreak') selects the second of these three nodes, which is the second linebreak processing instruction. The count of preceding (or preceding-sibling) processing instructions is 1, because the one you are dealing with is the second.
I'm not quite sure what you were thinking of, but I suspect your mistake is to think of "preceding" and "following" as selecting relative to the position of the node within the $node sequence, rather than relative to other nodes within the original source tree. I would recommend reading the section of an XPath reference book that describes the various axes.

check if repeating node is empty in xslt

I have xml like defined below . The node EducationDetails can repeat (unbounded).
<PersonalDetailsResponse>
<FirstName></FirstName>
<LastName></LastName>
<EducationDetails>
<Degree></Degree>
<Institution></Institution>
<Year></Year>
</EducationDetails>
<EducationDetails>
<Degree></Degree>
<Institution></Institution>
<Year></Year>
</EducationDetails>
</PersonalDetailsResponse>
I want to create another xml from the above one using xslt.
My requirement is, if there is no data in any of the EducationDetails child nodes , then the resulting xml has to get data from another source.
My problem is , I am not able to check if all the EducationDetails child nodes are empty.
Since variable value cannot be changed in xslt , I tried using saxon with below code.
xmlns:saxon="http://saxon.sf.net/" extension-element-prefixes="saxon"
<xsl:variable name="emptyNode" saxon:assignable="yes" select="0" />
<xsl:when test="count(ss:education) > 0">
<xsl:for-each select="ss:education">
<xsl:if test="not(*[.=''])">
<saxon:assign name="emptyNode">
<xsl:value-of select="1" />
</saxon:assign>
</xsl:if>
</xsl:for-each>
<xsl:if test="$emptyNode = 0">
<!-- Do logic if all educationdetails node is empty-->
</xsl:if>
</xsl:when>
But it throwing exception "net.sf.saxon.trans.XPathException: Unknown extension instruction " .
It looks like saxon 9 jar is required for it ,which I am not able to get from my repository.
Is there a simpler way to check if all the child nodes of are empty.
By empty I mean, child nodes might be present, but no value in them.
Well, if you use <xsl:template match="PersonalDetailsResponse[EducationDetails[*[normalize-space()]]">...</xsl:template> then you match only on PersonalDetailsResponse element having at least one EducationDetails element having at least one child element with non whitespace data. As you seem to use an XSLT 2.0 processor you can also use the perhaps clearer <xsl:template match="PersonalDetailsResponse[some $ed in EducationDetails/* satisfies normalize-space($ed)]">...</xsl:template>.
Or perhaps if you want a variable inside the template use
<xsl:template match="PersonalDetailsResponse">
<xsl:variable name="empty-details" select="not(EducationDetails/*[normalize-space()])"/>
<xsl:if test="$empty-details">...</xsl:if>
</xsl:template>
With XSLT 2.0 the use of some or every satisfies might be easier to understand e.g.
<xsl:template match="PersonalDetailsResponse">
<xsl:variable name="empty-details" select="every $dt in EducationDetails/* satisfies not(normalize-space($dt))"/>
<xsl:if test="$empty-details">...</xsl:if>
</xsl:template>
But usually writing templates with appropriate match conditions saves you from using xsl:if or xsl:choose inside of a template.

How to break XSLT for-each loop?

I know break is not possible in XSLT for-each loop.
But from following example, If I want to show 'something' element exist or not. Then I will go through loop and once I got it I want to break loop.
I will make one bool variable say isExist=false;
I will go through all nodes one by one.
Once I got 'something', I will change its value to isExist=true
want to break loop here.
Now I want to break loop bcoz I want to show status as true or false. But If I will not use break after step 3, it will go to loop and make change it to false and I will get status as false.
My question is: If break not possible then How to achieve this result?
I am following this link How to interrupt an XSLT for-each loop for not contiguos elements?
According to this link I will get first two occurrence but it doen't say 'something' exist or not.
<root>
<item/>
<item/>
<something/>
<item/>
</root>
If all you're after is whether <something/> exists or not, you could also just check using
<xsl:if test="/root/something">
// do fancy stuff...
</xsl:if>
That's not how variables work in XSLT - variables are always lexically scoped to their containing element, and once set they cannot be changed.
You need to learn to think more declaratively rather than procedurally - tell the processor what you want it to find rather than how you would look for it. The XPath expression boolean(/root/something) will give you what you want - true if there is a something element as a child of the root and false otherwise.
When you write a XSLT template that matches an element selector, for example:
<xsl:template match="entry"> .... </entry>
and you have a XML file like this one:
<entries>
<entry number="345" type="A"/>
<entry number="123" type="B"/>
<entry number="334" type="A"/>
<entry number="322" type="C"/>
</entries>
The XSLT template will be processed for all entry elements. So most of the time, you never need to use a for-each in XSLT. In the above example, the template will be processed 4 times, since there are 4 entry elements to match. Inside each one you can extract or print the data that's available in that context (such as the attributes, in this example).
You can usually also perform many conditional expressions without ever using an <xsl:if> or <xsl:choose>, by restricting what you need via XPath predicates.
If you add another template for the entry elements with a predicate like this:
<xsl:template match="entry[#type='A']"> .... </entry>
then it will override the entry templates for the entry elements that have a type attribute with the value A. The others will be processed by the template that has no predicate.
Giving preference to these tools, instead of for-each, if and choose, will help you think in a more functional oriented way and understand how to use XSLT better.
Since variables and params can't be changed, you can't use a regular for-loop to increment or decrement it, but in case you need one, it is possible to write a for loop that increments a variable in XSLT 1.0, but it has to be done recursively (using xsl:call-template). In this strategy, you don't assign a different value to a variable, but perform an operation inside a variable block which will set the final value for that variable.
See, for example, this stylesheet which calculates a factorial of a number. The param n in each recursion has a final value which doesn't change but is used as the data for another n param in the next recursion:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text"/>
<xsl:param name="number">5</xsl:param>
<xsl:template match="/">
<xsl:variable name="fac">
<xsl:call-template name="factorial">
<xsl:with-param name="n" select="$number"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$fac"/>
</xsl:template>
<xsl:template name="factorial">
<xsl:param name="n"/>
<xsl:choose>
<xsl:when test="$n > 1">
<xsl:call-template name="factorial">
<xsl:with-param name="n-1"/>
</xsl:call-template>
<xsl:value-of select="$n * $n - 1"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="1"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

xsl for-each to create an iterable node list

I need to iterate over the elements of an XML file in sorted order of their language field many times. What I try is to get an iterable list of the languages as follows:
<xsl:variable name="languages">
<xsl:for-each select="elem/FIELD[#NAME='language']">
<xsl:sort select="."/>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:variable>
While I can verify with a
<xsl:value-of select="$languages"/>
that the sorting works, I cannot iterate like
<xsl:for-each select="$langauges">...</xsl:for-each>
because the XSL processor complains that select expression does not evaluate to a node set.
Edit: Not sure whether this is important, but I have
<xsl:output encoding="UTF-8"
method="xml"
media-type="text/xml"
indent="yes" />
What do I have to insert in the loop to make the result into a node set? Is this at all possible?
Given you say that
XSL processor complains that select expression does not evaluate to a node set.
I assume you're using XSLT 1.0 rather than 2.0. In XSLT 1.0 when you declare a variable with content rather than a select attribute, the resulting variable contains something called a "result tree fragment" rather than a node set. You can apply value-of and copy-of to a RTF to send it to the output but you can't navigate into it using XPath expressions.
Most XSLT processors provide some sort of extension function to convert a RTF into a real node set - msxsl for the Microsoft processor or exslt for most others.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl">
<!-- .... -->
<xsl:variable name="languagesRTF">
<xsl:for-each select="elem/FIELD[#NAME='language']">
<xsl:sort select="."/>
<lang><xsl:value-of select="."/></lang>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="languages" select="exsl:node-set($languagesRTF)/lang" />
In XSLT 2.0 there is no distinction between result tree fragments and node sets - they're both treated as sequences - so you don't need the extension function in that version.
Try the following:
To declare variable:
<xsl:variable name="languages">
<xsl:for-each select="elem/FIELD[#NAME='language']">
<xsl:sort select="."/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>
And to loop:
<xsl:for-each select="$languages/*">
You need to convert the result tree fragment in your variable into a node-set, using the EXSLT node-set() function.
XSLT 1.0 allows you to process a node-set in sorted order, but it doesn't allow you to save a sorted sequence in a variable (the data model only has sets, not sequences). The only way you can save sorted data in 1.0 is to construct a new tree containing copies of the original elements in a different order, and then use the node-set() extension to make this tree processable.
This changes in XSLT 2.0, which has a data model based on sequences. In 2.0 you can save a sorted sequence of nodes in a variable without copying the nodes into a new tree.

XSLT: need alternative to document()-function for multi-source processing

I'm adapting an XSLT from a third party which transforms an arbitrary number of XMLs into a single HTML document. It's a pretty complex script and it will be revised in the future, so I'm trying to do a minimal adaptation in order to get it to work for our needs.
The following is a stripped down version of the XSLT (containing the essentials):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml">
<xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/>
<xsl:param name="files" select="document('files.xml')//File"/>
<xsl:param name="root" select="document($files)"/>
<xsl:template match="/">
<xsl:for-each select="$root/RootNode">
<xsl:apply-templates select="."/>
</xsl:for-each>
</xsl:template>
<xsl:template match="RootNode">
<xsl:for-each select="//Node">
<xsl:text>Node: </xsl:text><xsl:value-of select="."/><xsl:text>, </xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Now files.xml contains a list of all the URLs of the files to be included (in this case the local files file1.xml and file2.xml). Because we want to read XMLs from memory rather than from disk, and because the invocation of the XSLT only allows for a single XML source, I have combined the two files in a single XML document. The following is a combination of two files (there may be more in a real situation)
<?xml version="1.0" encoding="UTF-8"?>
<TempNode>
<RootNode>
<Node>1</Node>
<Node>2</Node>
</RootNode>
<RootNode>
<Node>3</Node>
<Node>4</Node>
</RootNode>
</TempNode>
where the first RootNode originally resided in file1.xml and the second in file2.xml.
Due to the complexity of the actual XSLT, I've figured that my best shot is to try to alter the $root-param. I've tried the following:
<xsl:param name="root" select="/TempNode"/>
The problem is this. In the case of <xsl:param name="root" select="document($files)"/>, the XPath expression "//Node" in <xsl:for-each select="//Node"> selects the Node's from file1.xml and file2.xml independently, i.e. producing the following (desired) list:
Node: 1, Node: 2, Node: 3, Node: 4,
However, when I combine the content of the two files into a single XML and parse this (and use the suggested $root-definition), the expression "//Node" will select all Node's that are children of the TempNode. (In other words, the desired list, as represented above, is produced twice due to the combination with the outer <xsl:for-each select="$root/RootNode"> loop.)
(A side note: as observed in comment a) in this page, document() apparently changes the root node, perhaps explaining this behavior.)
My question becomes:
How can I re-define $root, using the combined XML as source instead of a multi-source through document(), so that the list is only produced once, without touching the remainder of the XSLT? It's like if $root defined using the document()-function, there is no common root node in the param. Is it possible to define a param with two "separate" node trees?
Btw: I've tried defining a document like this
<xsl:param name="root">
<xsl:for-each select="/TempNode/*">
<xsl:document>
<xsl:copy-of select="."/>
</xsl:document>
</xsl:for-each>
</xsl:param>
thinking it might solve the problem, but the "//Node" expression still fetches all the Nodes. Is the context node in the <xsl:template match="RootNode">-template actually somewhere in the input document and not the param? (Honestly, I'm pretty confused when it comes to context nodes.)
Thanks in advance!
(Updated more)
OK, some of the problem is becoming clear. First, just to make sure I understand, you aren't actually passing parameters for $files and $root to the XSLT processor invocation, right? (They might as well be variables rather than params?)
Now to the main issues... In XPath, when you evaluate an expression that begins with "/" (including "//"), the context node is ignored [mostly]. Therefore, when you have
<xsl:template match="RootNode">
<xsl:for-each select="//Node">
the matched RootNode is ignored. Maybe you wanted
<xsl:template match="RootNode">
<xsl:for-each select=".//Node">
in which the for-each would select Node elements that are descendants of the matched RootNode? This would fix your problem of generating the desired node list twice.
I inserted [mostly] above because I recalled that an "absolute location path" starts from "the root node of the document containing the context node". So the context node does affect what document is used for "//Node". Maybe that's what you intended all along? I guess I was slow to catch on to that.
(A side note: as observed in comment
a) in this page, document() apparently
changes the root node, perhaps
explaining this behavior.)
Or more precisely,
An absolute location path ["/..."]
followed by a relative location
path... selects the set of nodes that
would be selected by the relative
location path relative to the root
node of the document containing the
context node.
document() doesn't actually change anything, in the sense of side effects; rather, it returns a set of nodes contained (usually) by different documents than the primary source document. XSLT instructions like xsl:apply-templates and xsl:for-each establish new values for the context node inside the scope of their template bodies. So if you use xsl:apply-templates and xsl:for-each with select="document(...)/...", the context node inside the scope of those instructions will belong to an external document, so any use of "/..." as an XPath will start from that external document.
Updated again
How can I re-define $root, using the
combined XML as source instead of a
multi-source through document(), so
that the list is only produced once,
without touching the remainder of the
XSLT?
As #Alej hinted, it's really not possible given the above constraint. If you're selecting "//Node" in each iteration of the loop over "$root/RootNode", then in order for each iteration not to select the same nodes as the other iterations, each value of "$root/RootNode" must be in a different document. Since you're using the combined XML source, instead of a multi-source, this is not possible.
But if you don't insist that your <xsl:for-each select="//..."> XPath expression cannot change, it becomes very easy. :-) Just put a "." before the "//".
It's like if $root defined using the document()-function, there is no common root node
in the param.
The value of the param is a node-set. All nodes in the set may be contained in the same document, or they may not, depending on whether the first argument to document() is a nodeset or just a single node.
Is it possible to define a param with two "separate" node trees?
I believe by "separate", you mean "belonging to different documents"? Yes it is, but I don't think you can do it in XSLT 1.0 unless you're selecting nodes that belong to different documents in the first place.
You mentioned trying
<xsl:param name="root">
<xsl:for-each select="/TempNode/*">
<xsl:document>
<xsl:copy-of select="."/>
</xsl:document>
</xsl:for-each>
</xsl:param>
but <xsl:document> is not defined in XSLT 1.0, and your stylesheet says version="1.0". Do you have XSLT 2.0 available? If so, let us know and we can pursue this option. To be honest, <xsl:document> is not familiar territory for me. But I'm happy to learn along with you.
You can apply only nodes you need:
Input:
<?xml version="1.0" encoding="UTF-8"?>
<TempNode>
<RootNode>
<Node>1</Node>
<Node>2</Node>
</RootNode>
<RootNode>
<Node>3</Node>
<Node>4</Node>
</RootNode>
</TempNode>
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<xsl:copy>
<xsl:apply-templates select="TempNode/RootNode"/>
</xsl:copy>
</xsl:template>
<xsl:template match="RootNode">
<xsl:value-of select="concat('RootNode-', generate-id(.), '
')"/>
<xsl:apply-templates select="Node"/>
</xsl:template>
<xsl:template match="Node">
<xsl:value-of select="concat('Node', ., '
')"/>
</xsl:template>
</xsl:stylesheet>
Output:
RootNode-N65540
Node1
Node2
RootNode-N65549
Node3
Node4