check if repeating node is empty in xslt - 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.

Related

XSLT 3.0: Create element only if input element has data

We are using XSLT internally to map a single input schema to a large number of distinct output schemas. Most of the servers using these schemas return errors on empty elements, so empty elements cannot appear in the output. In many cases, a piece of data in the input will simply map to a piece of data in the output, possibly with a minor transformation, e.g.:
<!-- Input -->
<ourns:DateCreated>2021-12-09</ourns:DateCreated>
<!-- Output -->
<otherns:CreatedDt>2021-12-09<otherns:CreatedDt>
The XSLT for this is straightforward, even with the "no empty elements" requirement:
<xsl:if test="ourns:DateCreated != ''">
<otherns:CreatedDt>
<xsl:value-of select="ourns:DateCreated/text()"/>
</otherns:CreatedDt>
</xsl:if>
However, when you're mapping thousands of elements across hundreds of schemas, this business of wrapping everything in <xsl:if/> gets tiresome. You could add a function, say:
<xsl:function name="ourfn:createElementIfData">
<xsl:param name="tag" as="xs:string"/>
<xsl:param name="data" as="xs:string"/>
<xsl:if test="$data != ''">
<xsl:element name="{$tag}"><xsl:value-of select="$data"/></xsl:element>
</xsl:if>
</xsl:function>
...
<xsl:sequence select="ourfn:createElementIfData('otherns:CreatedDt', ourns:DateCreated)"/>
But this function will only work if it lives in a stylesheet where both namespaces are declared. If you wanted to share it (as you probably would such a general-purpose function), you would end up needing to either
Declare every possible otherns in the shared stylesheet, or
Pass in the fully qualified namespace on every invocation,
both of which feel wrong.
This seems like such a common use case that I feel like there must be a simple way to do it. What am I missing?
You could define your basic rules like this:
<xsl:template match="ourns:DateCreated"
mode="copySimpleElement">
<otherns:CreatedDt>{.}</otherns:CreatedDt>
</xsl:template>
and then override it for empty elements:
<xsl:template match="*[. = '']"
mode="copySimpleElement"
priority="20"/>
and then you just have to apply-templates to the relevant elements in the appropriate mode.
You haven't shown any context but perhaps <xsl:template match="ourns:*[not(has-children())]"/> suffices to prevent any processing of the elements without content and adding <xsl:template match="ourns:DateCreated[has-children()]" expand-text="yes"><otherns:CreatedDt>{.}</otherns:CreatedDt></xsl:template> suffices to map the non-empty element to the wanted output element.
Of course <xsl:template match="ourns:*[not(has-children())]"/> could be set up as <xsl:template match="*[not(has-children())]"/> if the rule can be applied to input elements from any namespace or can take a sequence of patterns with <xsl:template match="ourns:*[not(has-children())] | ourns2:*[not(has-children())]"/>.
All the above assumes you are processing those nodes through other templates e.g. the identity transformation <xsl:mode on-no-match="shallow-copy"/>.
If you want to take the function approach I would check if you can pass in an xs:QName:
<xsl:function name="ourfn:createElementIfData">
<xsl:param name="node-name" as="xs:QName"/>
<xsl:param name="data" as="xs:string"/>
<xsl:if test="$data != ''">
<xsl:element name="{$node-name}" namespace="{namespace-uri-from-QName($node-name)}"><xsl:value-of select="$data"/></xsl:element>
</xsl:if>
</xsl:function>
and use e.g. <xsl:sequence select="ourfn:createElementIfData(QName('http://yourothernamespace/', 'otherns:CreatedDt'), ourns:DateCreated)"/>.

Testing text node in XSLT

I have the following mix-content element:
<firstElement type="random">text1<secondElement>random_value</secondElement>text2</firstElement>
I want to make a for-each loop on <firstElement> child nodes with a nested if condition, for example:
<xsl:for-each select="child::*">
<xsl:if test="some test">
<xsl:copy>
</xsl:if>
</xsl:for-each>
How can I write a test that selects only the text nodes of <firstElement>?
I have tried with text(), but it only works for the child text node of the contextual node and self::text() doesn't seem to be a proper Xpath. I also tried to use XSLT 2.0 instance of to test against xs:string but it didn't work either.
How can I write a test that selects only the text nodes of
<firstElement>?
It is really difficult to understand your question:
First, a test does not select anything - I suppose you mean you want the test to pass text nodes only.
Next, when you do:
<xsl:for-each select="child::*">
you are selecting element nodes only - so any subsequent test that passes text nodes only will always return false.
It's also not clear why you need to select any nodes that are not text nodes in the first place, and then test them and pass only text nodes. But supposing that's really what you want to do, you can do it this way:
<xsl:template match="firstElement">
<output>
<!-- select all child nodes -->
<xsl:for-each select="node()">
<!-- pass only text nodes -->
<xsl:if test=". instance of text()">
<xsl:copy/>
</xsl:if>
</xsl:for-each>
</output>
</xsl:template>
This returns:
<output>text1text2</output>
which is the same result returned by the much simpler:
<xsl:template match="firstElement">
<output>
<xsl:copy-of select="text()"/>
</output>
</xsl:template>
I have tried with text(), but it only works for the child text node of the contextual node and self::text() doesn't seem to be a proper Xpath.
I am not sure why you think so.
<xsl:if test="self::text()">
works just as well - and has the advantage of being backward-compatible with XSLT 1.0.

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>

Find the position of an element within its parent with XSLT / XPath

Apart from rewriting a lot of XSLT code (which I'm not going to do), is there a way to find the position of an element within its parent, when the context is arbitrarily set to something else? Here's an example:
<!-- Here are my records-->
<xsl:for-each select="/path/to/record">
<xsl:variable name="record" select="."/>
<!-- At this point, I could use position() -->
<!-- Set the context to the current record -->
<xsl:for-each select="$record">
<!-- At this point, position() is meaningless because it's always 1 -->
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
</xsl:for-each>
<!-- This template expects the current context being set to a record -->
<xsl:template name="SomeTemplate">
<!-- it does stuff with the record's fields -->
<xsl:value-of select="SomeRecordField"/>
<!-- How to access the record's position in /path/to or in any other path? -->
</xsl:template>
NOTE: This is a simplified example. I have several constraints keeping me from implementing obvious solutions, such as passing new parameters to SomeTemplate, etc. I can really only modify the internals of SomeTemplate.
NOTE: I'm using Xalan 2.7.1 with EXSLT. So those tricks are available
Any ideas?
You could use
<xsl:value-of select="count(preceding-sibling::record)" />
or even, generically,
<xsl:value-of select="count(preceding-sibling::*[name() = name(current())])" />
Of course this approach will not work if you process a list of nodes that is not uniform, i.e.:
<xsl:apply-templates select="here/foo|/somewhere/else/bar" />
Position information is lost in such a case, unless you store it in a variable and pass that to the called template:
<xsl:variable name="pos" select="position()" />
<xsl:for-each select="$record">
<xsl:call-template name="SomeTemplate">
<xsl:with-param name="pos" select="$pos" />
</xsl:call-template>
</xsl:for-each>
but obviously that would mean some code rewriting, which I realize you want to avoid.
Final hint: position() does not tell you the position of the node within its parent. It tells you the position of the current node relative to the list of nodes you are processing right now.
If you only process (i.e. "apply templates to" or "loop over") nodes within one parent, this happens to be the same thing. If you don't, it's not.
Final hint #2: This
<xsl:for-each select="/path/to/record">
<xsl:variable name="record" select="."/>
<xsl:for-each select="$record">
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
</xsl:for-each>
is is equivalent to this:
<xsl:for-each select="/path/to/record">
<xsl:call-template name="SomeTemplate"/>
</xsl:for-each>
but the latter works without destroying the meaning of position(). Calling a template does not change context, so . will refer to the correct node withing the called template.

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>