XSLT Get First Element Node - xslt

<SMRCRLT_XML>
<AREA>
<DETAILS>
<DETAIL_REQUIREMENT>
<RULE_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>A</COURSE_SET>
<COURSE_SUBSET>1</COURSE_SUBSET>
<COURSE_SUBJ_CODE>CHEM</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>345A</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>A</COURSE_SET>
<COURSE_SUBSET>2</COURSE_SUBSET>
<COURSE_SUBJ_CODE>CHEM</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>476A</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>A</COURSE_SET>
<COURSE_SUBSET>3</COURSE_SUBSET>
<COURSE_SUBJ_CODE>PHIL</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>432</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>B</COURSE_SET>
<COURSE_SUBSET>4</COURSE_SUBSET>
<COURSE_SUBJ_CODE>PHIL</COURSE_SUBJ_CODE>
<COURSE_SUBJ_DESC>Philosophy</COURSE_SUBJ_DESC>
<COURSE_CRSE_NUMB_LOW>433</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>B</COURSE_SET>
<COURSE_SUBSET>5</COURSE_SUBSET>
<COURSE_SUBJ_CODE>ZOOL</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>321</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>B</COURSE_SET>
<COURSE_SUBSET>6</COURSE_SUBSET>
<COURSE_SUBJ_CODE>BIOC</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>456</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
</RULE_REQUIREMENT>
</DETAIL_REQUIREMENT>
</DETAILS>
</AREA>
</SMRCRLT_XML>
I am trying to get the first element from the XML for each COURSE_SET, but it returns all the values. Can someone please help. This is my template that I applied:
<xsl:apply-templates select="//SMRCRLT_XML/AREA/DETAILS/DETAIL_REQUIREMENT/RULE_REQUIREMENT/DETAIL_REQUIREMENT/COURSE_ROWSET/COURSE_SET[COURSE_AREA='TESTSELECT' and COURSE_KEY_RULE='1200'][1]"/>
The results I am getting are:
CHEM345A
PHIL432
PHIL433
ZOOL321
BIOC456
The result I am looking for is CHEM 345A and then PHIL433

You have several problems here.
First, the [1] in your XPath expression is filtering the XPath value by requiring that the COURSE_SET elements selected be the first child of their parent. Without that [1], your XPath expression reads:
//SMRCRLT_XML
/AREA
/DETAILS
/DETAIL_REQUIREMENT
/RULE_REQUIREMENT
/DETAIL_REQUIREMENT
/COURSE_ROWSET
/COURSE_SET
[COURSE_AREA='TESTSELECT' and COURSE_KEY_RULE='1200']
But every COURSE_SET that matches that path expression is the first child of its parent. (The only COURSE_SET elements which are not first children are children of COURSE_SET, not children of COURSE_ROWSET.)
The second problem is that it appears, judging by your question and your attempt at formulating the XPath expression you want, that you would like the courses to be grouped somehow (at first I thought you might want them grouped by department but now I expect you want them grouped by the value of the nested COURSE_SET element, which in your example has values A or B), so that by selecting the first COURSE_SET in some suitable context you can get the first course listed for each group. But the XML you show doesn't in fact group the courses by department or by course set; it provides a flat list of courses with no groupings at all. There are no elements here for which CHEM 345A and PHIL 433 are the first courses.
If your design calls for the courses to be grouped by department or course set, then your data source is not providing the data you want, and you will want to fix it.
If on the other hand you're stuck with this XML and want to use XPath to try to provide the structure that your data source is not capable of providing, then you don't want "the first element for each COURSE_SET", you want "each COURSE_SET which is in a department (or a COURSE_SET) different from the immediately preceding COURSE_SET". And your XPath expression can be something like
//COURSE_ROWSET/COURSE_SET
[not(COURSE_SET eq preceding::COURSE_SET[1])]
Your third problem is that your XML seems to be too fond of using the same name for different constructs (one set of COURSE_SET elements each of which contains a description of a course, with department and course number and so on, and a second set of COURSE_SET elements which contain the strings 'A' and 'B', two sets of DETAIL_REQUIREMENT with different content, and so on. It's confusing for people not familiar with the data, and it will make every single discussion of detail an opportunity for miscommunication and error.

The efficient way to handle a task like this in XSLT 1.0 is to use Muenchian grouping, like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:key name="kSet" match="COURSE_ROWSET/COURSE_SET" use="COURSE_SET" />
<xsl:template match="/">
<root>
<xsl:apply-templates
select="//COURSE_ROWSET/COURSE_SET[generate-id() =
generate-id(key('kSet', COURSE_SET)[1])]" />
</root>
</xsl:template>
<xsl:template match="COURSE_ROWSET/COURSE_SET">
<item>
<xsl:value-of select="concat(COURSE_SUBJ_CODE, COURSE_CRSE_NUMB_LOW)"/>
</item>
</xsl:template>
</xsl:stylesheet>
When this XSLT is applied to your sample input, the result is:
<root>
<item>CHEM345A</item>
<item>PHIL433</item>
</root>

Related

Constructing, not selecting, XSL node set variable

I wish to construct an XSL node set variable using a contained for-each loop. It is important that the constructed node set is the original (a selected) node set, not a copy.
Here is a much simplified version of my problem (which could of course be solved with a select, but that's not the point of the question). I've used the <name> node to test that the constructed node set variable is in fact in the original tree and not a copy.
XSL version 1.0, processor is msxsl.
Non-working 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="text" encoding="iso-8859-1" omit-xml-declaration="yes" />
<xsl:template match="/">
<xsl:variable name="entries">
<xsl:for-each select="//entry">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="entryNodes" select="msxsl:node-set($entries)"/>
<xsl:for-each select="$entryNodes">
<xsl:value-of select="/root/name"/>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
XML input:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<name>X</name>
<entry>1</entry>
<entry>2</entry>
</root>
Wanted output:
X1X2
Actual output:
12
Of course the (or a) problem is the copy-of, but I can't work out a way around this.
There isn't a "way around it" in XSLT 1.0 - it's exactly how this is supposed to work. When you have a variable that is declared with content rather than with a select then that content is a result tree fragment consisting of newly-created nodes (even if those nodes are a copy of nodes from the original tree). If you want to refer to the original nodes attached to the original tree then you must declare the variable using select. A better question would be to detail the actual problem and ask how you could write a suitable select expression to find the nodes you want without needing to use for-each - most uses of xsl:if or xsl:choose can be replaced with suitably constructed predicates, maybe involving judicious use of xsl:key, etc.
In XSLT 2.0 it's much more flexible. There's no distinction between node sets and result tree fragments, and the content of an xsl:variable is treated as a generic "sequence constructor" which can give you new nodes if you construct or copy them:
<xsl:variable name="example" as="node()*">
<xsl:copy-of select="//entry" />
</xsl:variable>
or the original nodes if you use xsl:sequence:
<xsl:variable name="example" as="node()*">
<xsl:sequence select="//entry" />
</xsl:variable>
I wish to construct an XSL node set variable using a contained
for-each loop.
I have no idea what that means.
It is important that the constructed node set is the original (a
selected) node set, not a copy.
This part I think I understand a little better. It seems you need to replace:
<xsl:variable name="entries">
<xsl:for-each select="//entry">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>
with:
<xsl:variable name="entries" select="//entry"/>
or, preferably:
<xsl:variable name="entries" select="root/entry"/>
The resulting variable is a node-set of the original entry nodes, so you can do simply:
<xsl:for-each select="$entries">
<xsl:value-of select="/root/name"/>
<xsl:value-of select="."/>
</xsl:for-each>
to get your expected result.
Of course, you could do the same thing by operating directly on the original nodes, in their original context - without requiring the variable.
In response to the comments you've made:
We obviously need a better example here, but I think I am getting a vague idea of where you want to go with this. But there are a few things you must understand first:
1.
In order to construct a variable which contains a node-set of nodes in their original context, you must use select. This does not place any limits whatsoever on what you can select. You can do your selection all at once, or in stages, or even in a loop (here I mean a real loop). You can combine the intermediate selections you have made in any way sets can be combined: union, intersection, or difference. But you must use select in all these steps, otherwise you will end up with a set of new nodes, no longer having the context they did in the source tree.
IOW, the only difference between using copy and select is that the former creates new nodes, which is precisely what you wish to avoid.
2.
xsl:for-each is not a loop. It has no hierarchy or chronology. All the nodes are processed in parallel, and there is no way to use the result of previous iteration in the current one - because no iteration is "previous" to another.
If you try to use xsl:for-each in order to add each of n processed nodes to a pre-existing node-set, you will end up with n results, each containing the pre-existing node-set joined with one of the processed nodes.
3.
I think you'll find the XPath language is quite powerful, and allows you to select the nodes you want without having to go through the complicated loops you hint at.
It might help if you showed us a problem that can't be trivially solved in XSLT 1.0. You can't solve your problem the way you are asking for: there is no equivalent of xsl:sequence in XSLT 1.0. But the problem you have shown us can be solved without such a construct. So please explain why you need what you are asking for.

XSLT: for-each loop with key not returning all nodes

I am a novice XSLT developer. I have been asked to fix an issue on a project where the original developer is no longer with us. In the XSLT, there is a for-each loop using a key and a count
<xsl:for-each select="ns0:BOM[count(. | key('subsat', ns0:BomText01)[1]) = 1][ns0:BomText01]">
...
This is the key:
<xsl:key name="subsat" match="ns0:Parts/ns0:BOM[ns0:FindNum!='0']" use="ns0:BomText01" />
In the XML file being transformed, there are two sibling nodes that represent sub-parts:
<ns0:BOM referentId="10000:65091335:65359080">
<ns0:BomText01>3069260-303-SUB0027</ns0:BomText01>
<ns0:ItemNumber>My_part_1</ns0:ItemNumber>
<ns0:ItemType>Part</ns0:ItemType>
<ns0:Qty>67</ns0:Qty>
</ns0:BOM>
<ns0:BOM referentId="10000:65102551:86713230">
<ns0:BomText01>3069260-303-SUB0027</ns0:BomText01>
<ns0:ItemNumber>My_part_2</ns0:ItemNumber>
<ns0:ItemType>Part</ns0:ItemType>
<ns0:Qty>67</ns0:Qty>
</ns0:BOM>
However, the loop is only picking up the first node (My_part_1). I suspect it's because of the count=1 but I really don't know. And I don't know how to modify it. Ideas? If I need to include more data, let me know.
Assuming that the relevant part of your XSLT looks something like this:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns0="ns0" version="1.0">
<xsl:key name="subsat" match="ns0:BOM[ns0:FindNum!='0']" use="ns0:BomText01"/>
<xsl:template match="ns0:Parts">
<xsl:for-each
select="ns0:BOM[count(. | key('subsat', ns0:BomText01)[1]) = 1][ns0:BomText01]">
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
It will only print the first of the elements because it is selecting the BOM elements which have an unique BomText01 value. That's the expected result.
If the BomText01 is an ID field (as it seems it is) and you expected to get both result (perhaps, because their ItemNumber contains different values), the error is possibly in your source (which assigned equal IDs when it should not have done so).
If you change one of those values in the source, you should be able to select both and verify this.

Counting elements that are generated in XSLT1

I'm trying to count the elements my transformation generates (must use XLST1). For example, my transformation creates:
<Parent>
<ElementX Att1="2"/>
<ElementY Att1="1"/>
<ElementZ Att1="6"/>
</Parent>
I need to print 3 within the same transformation, because there are 3 child elements.
Can this be done?
Thanks.
It would help a lot if you provide some extract of your XSLT.
I cn't give you a XSLT code without it. I'll try to give some "way" to the answer :
One solution could be to store the output into a nodeset (use the XSLT 1.0 extension which provides the nodeset() function) and apply the XPath count() function on this variable. After that just output your variable with xsl:value-of, and your count result the same way.
Here is a demo how to do this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="vrtfPass1">
<xsl:apply-templates/>
</xsl:variable>
<xsl:value-of select="count(ext:node-set($vrtfPass1)/*/*)"/>
</xsl:template>
<xsl:template match="/*">
<Parent>
<ElementX Att1="2"/>
<ElementY Att1="1"/>
<ElementZ Att1="6"/>
</Parent>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on any XML document (not used in this Demo), the wanted, correct result is produced:
3
Explanation:
A general way to process the result of the transformation (in a single transformation), is to organize it in two passes where we save the result of the first pass in a variable.
In the second pass we access the result and do the additional processing.
Do note that in XSLT 1.0 if the variable that captures the result of the first pass is of the infamous RTF (Result Tree Fragment) type and needs to be converted to a regular tree in order of any nodes inside this tree to be accessible (xsl:copy-of and string() are still allowed on an RTF).
This conversion to a regular tree is done by an extension function, which most often has the name node-set and always belongs to a vendor-defined namespace. In this demo we are using the node-set() extension function that belongs to the EXSLT namespace -- because most XSLT 1.0 processors implement EXSLT.
For more information on multi-pass processing, see this: Two phase processing: Do not output empty tags from phase-1 XSLT 2.0 processing

selecting one of several elements that differ in attribute

I'm an experienced programmer, but a novice to XSLT and am finding it quite baffling. I apologize if this is a question that's been asked before, but I'm so frustrated by XSLT that I'm not even sure what to search for...
I have a problem that if a certain XML element appears only once, I want its contents output, but if it occurs more than once, I want only the contents of those that have a certain attribute.
For example, suppose I have one XML file (call it "file 1") that contains
<food>
<snack>Chips</snack>
<snack type="nuts">Peanuts</snack>
</food>
and another XML file ("file 2") that contains
<food>
<snack>Cheese puffs</snack>
</food>
I need an XSLT that outputs only "Peanuts" (but not "Chips") upon processing file 1, but still outputs "Cheese puffs" for file 2 (i.e. I can't just select only those elements that have a "type" attribute, that would be too easy).
This is probably simple, but I'm stuck...
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match=
"*[snack/#type]/snack[not(#type)]"/>
</xsl:stylesheet>
produces the wanted results in both cases.
Explanation:
The identity rule/template copies every node "as-is".
The second template is overriding the identity template for any snack element without a type attribute that has sibling snack elements that have a type attribute. This template has empty body, which effectively "deletes" the matched element from (being copied to) the output.
if a certain XML element appears only once, I want its contents output, but if it occurs more than once, I want only the contents of those that have a certain attribute.
A direct translation of that would be
if (count(snack) = 1) then snack else snack[#type='nuts']
which is valid XPath 2.0 syntax - if you need to do it in 1.0 then it translates fairly directly (though verbosely) into an equivalent xsl:choose.
If you want something even more concise than the above, you can also write in XPath 2.0
(snack[#type='nuts'], snack)[1]
which builds a list containing first the snacks with type='nuts', then all the snacks, and then selects the first item from this list.

How to concatenate two node-sets such that order is respected?

My understanding has been that, despite the fact that XSLT's "node-sets" are called "sets", they are, in fact, ordered lists of nodes (which is why each node is associated with an index). I've therefore been trying to use the "|" operator to concatenate node-sets such that the order of the nodes is respected.
What I am attempting to accomplish is something like the following JavaScript code:
[o1,o2,o3].concat([o4,o5,o6])
Which yields:
[o1,o2,o3,o4,o5,o6]
But, consider the following reduced example:
testFlatten.xsl
<?xml version="1.0"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="xml"/>
<xsl:template match="/">
<xsl:variable name="parentTransition" select="//*[#id='parentTransition']"/>
<xsl:variable name="childTransition" select="//*[#id='childTransition']"/>
<xsl:variable name="parentThenChildTransitions" select="$parentTransition | $childTransition"/>
<xsl:variable name="childThenParentTransitions" select="$childTransition | $parentTransition"/>
<return>
<parentThenChildTransitions>
<xsl:copy-of select="$parentThenChildTransitions"/>
</parentThenChildTransitions>
<childThenParentTransitions>
<xsl:copy-of select="$childThenParentTransitions"/>
</childThenParentTransitions>
</return>
</xsl:template>
</xsl:stylesheet>
Given the following input:
<?xml version="1.0"?>
<root>
<element id="parentTransition"/>
<element id="childTransition"/>
</root>
Which yields (with xsltproc):
<?xml version="1.0"?>
<return>
<parentThenChildTransitions>
<element id="parentTransition"/><element id="childTransition"/>
</parentThenChildTransitions>
<childThenParentTransitions>
<element id="parentTransition"/><element id="childTransition"/>
</childThenParentTransitions>
</return>
So the "|" operator in fact does not respect the order of the node-set operands. Is there a way I can concatenate node-sets such that order is respected?
This is actually not an XSLT but an XPath question.
In XPath 1.0 there isn't anything similar to a "list" datatype. A node-set is a set and it has no order.
In XPath 2.0 there is the sequence data type. Any items in a sequence are ordered. This has nothing to do with document order. Also, the same item (or node) can appear more than once in a sequence.
So, in XSLT 2.0 one just uses the XPath 2.0 sequence concatenation operator ,:
//*[#id='parentTransition'] , //*[#id='childTransition']
and this evaluates to the sequence of all elements in the document with id attribute 'parentTransition' followed by all elements in the document with id attribute 'childTransition'
In XSLT it is still possible to access and process nodes not in document order: for example using the <xsl:sort> instruction -- however the set of nodes that are processed as result of <xsl:apply-templates> or <xsl:for-each> is a node-list -- not a node-set.
Another example of evaluating nodes not in document order is the position() function within <xsl:apply-templates> or <xsl:for-each> that have a <xsl:sort> child or within a predicate of a location step (of an XPath expression) in which a reverse axis is used (such as ancesstor:: or preceeding::)
In XSLT 1.0, you can process nodes in a selected order (for example by use of xsl:sort), but you can't hold a list of nodes in a variable. The only thing you can hold in a variable (or pass to a template, etc) is a node-set; node-sets have no intrinsic order, but when you process them, they are always processed in document order unless you use xsl:sort to request a different processing order.
You might be able to solve your problem by copying the nodes:
<xsl:variable name="temp">
<xsl:copy-of select="$ns0"/>
<xsl:copy-of select="$ns1"/>
</xsl:variable>
...
<xsl:apply-templates select="exslt:node-set($temp/*)"/>
but this depends on your use-case.
Switch to XSLT 2.0 if you can!
The "|" operator will retain nodes in document order. In XSLT 1.0 you will need to have sequential copy or for-each operations.
<xsl:copy-of select="$parentTransition"/>
<xsl:copy-of select="$childTransition"/>