How to break XSLT for-each loop? - xslt

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>

Related

How to use for-each based on conditions - XSLT?

I have to select the values based on conditions. If Author name = Bero, it should display roles of 1st author, if Author name= Aurora it should display all topics.But I'm not getting expected output.
Here is my XSLT:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:regexp="http://exslt.org/regular-expressions"
version="1.0">
<xsl:template match="/">
<xsl:variable name="Store">
<Bookstore>
<Author name="Bero">
<topic>Athletics</topic>
</Author>
<Author name="ABC">
<topic>Sports</topic>
</Author>
</Bookstore>
</xsl:variable>
<xsl:for-each select="$Store/Bookstore/Author">
<xsl:choose>
<xsl:when test="contains($Author ='Bero')">
<xsl:variable name="Store" select="string($Store/Bookstore/Author[#name='Aurora']/u/text())"/>
</xsl:when>
<xsl:when test="contains($Author ='Aurora')">
<xsl:variable name="Store" select="string($Store/Bookstore/Author[#name='Aurora'and #name='Bero']/u/text())"/>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Expected Partial Output1 when 1st test case is executed:
<topic>Athletics</topic>
Expected Partial Output2:
<topic>Athletics</topic>
<topic>Sports</topic>
You're treating variables as you would in a procedural language. See for example xslt variable scope and its usage for a better understanding of why this doesn't work.
You're making two basic errors:
xsl:for-each is a functional mapping, not a loop. Think of it as processing all the selected items in parallel. The way you process one item can't have any effect on the way you process subsequent items
xsl:variable is a variable binding, not an assignment. Each xsl:variable instruction creates a new variable, it doesn't modify the value of other existing variables even if they have the same name. And when the xsl:variable instruction is the only thing inside xsl:when, then it has no effect at all, because the new variable disappears as soon as it is created.
Any good XSLT textbook will explain these concepts for you.
In addition, the condition <xsl:when test="contains($Author ='Bero')"> makes no sense. There's no variable named $Author, and if there was, the contains() function expects two string-valued arguments whereas you have supplied a single boolean argument.

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 can I access an xsl:variable from outside the "scope" of an xsl:for-each block

I'm trying to access a variable from within a for-each. I've done a lot of reading today to try to figure this out, but nothing quite fits my scenario. Eventually, I will have multiple series like seen below and I will use the variables that I'm pulling out and make different condition. The for-each that I have below is bringing back data from 400 records. I'm sorry, I cannot provide an XML. I'm not able to expose GUIDs and such.
UPDATE
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output media-type="xml" indent="yes"/>
<xsl:template match="/">
<Records>
<xsl:for-each select="Records/Record/Record[#levelGuid = 'level1']">
<xsl:variable name="rocName1" select="Field[#guid = '123']"/>
<xsl:variable name ="rocName2" select="substring-before($rocName1, ' - ')"/>
</xsl:for-each>
<xsl:for-each select="Records/Record/Record[#levelGuid = 'levelA']">
<xsl:variable name ="findingName" select="Field[#guid = '123']"/>
<xsl:variable name="findingName1" select="substring-after($findingName, ': ')"/>
<xsl:variable name="findingName2" select="substring-after($findingName1, 'PCIDSSv3.1:')"/>
</xsl:for-each>
<xsl:if test="$findingName1 = $rocName1">
<Records>
<findingID>
<xsl:for-each select="Records/Record/Record[#levelGuid = '123']">
<xsl:value-of select ="Field[#guid = '123']"/>
</xsl:for-each>
</findingID>
</Records>
</xsl:if>
</Records>
</xsl:template>
</xsl:stylesheet>
The desired output is any findingID that has a $findingName1 that equals $rocName1. The GUIDS only appear once, but each level has hundreds of records.
I'm trying to access a variable from within a for-each.
The variable $rocRecord is in scope inside the for-each. You can simply reference it and use it.
But I think you are trying to do something else. I.e., defining a variable inside for-each and wanting to use it outside it.
Variables are scoped within their focus-setting containing block. So the short answer is: you cannot do it. The long answer however...
Use templates. The only reason to do what you seem to want to be doing is to need to access the data elsewhere:
<xsl:template match="/">
<!-- in fact, you don't need for-each at all, but I leave it in for clarity -->
<xsl:for-each select="records/record">
<!--
apply-templates means: call the declared xsl:template that
matches this node, it is somewhat similar to a function call in other
languages, except that it works the other way around, the processor will
magically find the "function" (i.e., template) for you
-->
<xsl:apply-templates select="Field[#guid='123']" />
</xsl:for-each>
</xsl:template>
<xsl:template match="Field">
<!-- the focus here is what is the contents of your variable $rocName1 -->
<rocName>
<xsl:value-of select="substring=-before(., ' - ')" />
</rocName>
</xsl:template>
XSLT is a declarative, template-oriented, functional language with concepts that are quite unique compared to most other languages. It can take a few hours to get used to it.
You said you did a lot of reading, but perhaps it is time to check a little XSLT course? There are a few online, search for "XSLT fundamentals course". It will save you hours / days of frustration.
This is a good, short read to catch up on variables in XSLT.
Update
On second read, I think it looks like you are troubled by the fact that the loop goes on for 400 items and that you only want to output the value of $rocName1. The example I showed above, does exactly that, because apply-templates does nothing if the selection is empty, which is what happens if the guid is not found.
If the guid appears once, the code above will output it once. If it appears multiple times and you only want the first, append [1] to the select statement.
Update #2 (after your update with an example)
You have two loops:
<xsl:for-each select="Records/Record/Record[#levelGuid = 'level1']">
and
<xsl:for-each select="Records/Record/Record[#levelGuid = 'levelA']">
You then want to do something (created a findingId) when a record in the first loop matches a record in the second loop.
While you can solve this using (nested) loops, it is not necessary to do so, in fact, it is discouraged as it will make your code hard to read. As I explained in my original answer, apply-templates is usually the easier way to do get this to work.
Since the Record elements are siblings of one another, I would tackle this as follows:
<xsl:template match="/">
<Records>
<xsl:apply-templates select="Records/Records/Record[#levelGuid = 'level1']" />
</Records>
</xsl:template>
<xsl:template match="Record">
<xsl:variable name="rocName1" select="Field[#guid = '123']"/>
<xsl:variable name ="rocName2" select="substring-before($rocName1, ' - ')"/>
<xsl:variable name="findingNameBase" select="../Record[#levelGuid = 'levelA']" />
<xsl:variable name ="findingName" select="$findingNameBase/Field[#guid = '123']"/>
<xsl:variable name="findingName1" select="substring-after($findingName, ': ')"/>
<xsl:variable name="findingName2" select="substring-after($findingName1, 'PCIDSSv3.1:')"/>
<findingId rocName="{$rocName1}">
<xsl:value-of select="$findingName" />
</findingId>
</xsl:template>
While this can be simplified further, it is a good start to learn about applying templates, which is at the core of anything you do with XSLT. Learn about applying templates, because without it, XSLT will be very hard to understand.

XSLT position() - exlude elements that fail test condition

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 />

XSL: How best to store a node in a variable and then use it in future xpath expressions?

I need to be able to store a node set in variable and then perform more filting/sorting on it afterward. All the examples I've seen of this involve either using XSL2 or extensions neither of which are really an option.
I've a list of hotels in my XML doc that can be sorted/filtered and then paged through 5 at a time. I'm finding though I'm repeating alot of the logic as currently I've not found a good way to store node-sets in xsl variable and then use xpath on them for further filtering/sorting.
This is the sort of thing I'm after (excuse the code written of the top of my head so might not be 100%):
<xsl:variable name="hotels" select="/results/hotels[active='true']" />
<xsl:variable name="3_star_or_less" select="/results/hotels[number(rating) <= 3]" />
<xsl:for-each select="3_star_or_less">
<xsl:sort select="rating" />
</xsl:for-each>
Has anyone got an example of how best to do this sort of thing?
Try this example:
<xsl:variable name="hotels" select="/results/hotels[active='true']" />
<xsl:variable name="three_star_or_less"
select="$hotels[number(rating) <= 3]" />
<xsl:for-each select="$three_star_or_less">
<xsl:sort select="rating" />
<xsl:value-of select="rating" />
</xsl:for-each>
There is no problem storing a node-set in a variable in XSLT 1.0, and no extensions are needed. If you just use an XPath expression in select attribute of xsl:variable, you'll end up doing just that.
The problem is only when you want to store the nodes that you yourself had generated in a variable, and even then only if you want to query over them later. The problem here is that nodes you output don't have type "node-set" - instead, they're what is called a "result tree fragment". You can store that to a variable, and you can use that variable to insert the fragment into output (or another variable) later on, but you cannot use XPath to query over it. That's when you need either EXSLT node-set() function (which converts a result tree fragment to a node-set), or XSLT 2.0 (in which there are no result tree fragments, only sequences of nodes, regardless of where they come from).
For your example as given, this doesn't seem to be a problem. Rubens' answer gives the exact syntax.
Another note, if you want to be able to use the variable as part of an XPath statement, you need to select into the variable with <xsl:copy-of select="."/> instead of <xsl:value-of select="."/>
value-of will only take the text of the node and you wont be able to use the node-set function to return anything meaningful.
<xsl:variable name="myStringVar">
<xsl:value-of select="."/>
</xsl:variable>
<!-- This won't work: -->
<Output>
<xsl:value-of select="node-set($myStringVar)/SubNode" />
</Output>
<xsl:variable name="myNodeSetVar">
<xsl:copy-of select="."/>
</xsl:variable>
<!-- This will work: -->
<Output>
<xsl:value-of select="node-set($myNodeSetVar)/SubNode" />
</Output>