How to check for atomic item in XPATH? - xslt

i would like to check a sequence of Items and report the item type:
If it is an atomic value (maybe an empty string) report String "A"
If it is an Element report its local Name
Otherwise report String "X"
My Question is: How can i check an item to be an atomic value? The only Solution that i found is to use the xs:string(.) function, which takes an anyAtomic as argument. See the following XSLT 3.0 fragment
<xsl:template match="/*">
<xsl:variable name="content" as="item()*">
<xsl:apply-templates select="node()"/>
</xsl:variable>
<result>
<xsl:for-each select="$content">
<xsl:choose>
<!-- I`d like to check for any atomic value here -->
<xsl:when test="xs:string(.)">A</xsl:when>
<xsl:when test="self::*"><xsl:value-of select="lower-case(local-name())"/></xsl:when>
<xsl:otherwise>X</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</result>
</xsl:template>
<xsl:template match="*">
<xsl:copy/>
</xsl:template>
<xsl:template match="text()" as="xs:string">
<!-- What if i change to normalize-space(.) ? -->
<xsl:sequence select="."/>
</xsl:template>
Problem is, that i get an error when i use the normalize-space() function in the template matching text-Node, because in this case the effective boolean value of xs:string() is false for some text Nodes. The following check for the self Axis fails with the error message XPTY0020: The required item type of the context item for the self axis is node(); the supplied value "" is an atomic value.
I have tried with expressions like ". is anyAtomic", but the IS Operator expects Nodes.
I am pretty sure that xs:string(.) is not the correct way to check for atomic items, but what is it?
Thanks,
Frank Steimke

XPath has an instance of operator you can use with sequence types so I think you are looking for . instance of xs:anyAtomicType or e.g. . instance of xs:string.

Related

XSLT - How to refer to a current node value using xsl:choose?

I try to create a variable, which I can use in a later template:
<xsl:variable name="fc">
<xsl:choose>
<xsl:when test="self::node()='element1'">gray</xsl:when>
<xsl:otherwise>red</xsl:otherwise>
</xsl:choose>
</xsl:variable>
Unfortunately it does not work.
<xsl:template match="element1">
<h1><font color="{$fc}"><xsl:value-of select="self::node()"/></font></h1>
</xsl:template>
What am I doing wrong?
Here is the extensive code:
XML:
<root
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.test.com scheme.xsd" xmlns="http://www.test.com" xmlns:tst="http://www.test.com">
<elementA>
<elementB tst:name="name">
<elementC tst:name="name">
<element1> Test1 </element1>
<element2> Test2 </element2>
</elementC >
</elementB>
</elementA>
</root>
All the elements are qualified and part of the namespace "http://www.test.com".
XSLT:
<xsl:template match="/">
<html>
<body><xsl:apply-templates select="tst:root/tst:elementA/tst:elementB/tst:elementC/tst:element1"/>
</body>
</html>
</xsl:template>
<xsl:variable name="var_fc">
<xsl:choose>
<xsl:when test="local-name(.)='tst:element1'">gray</xsl:when>
<xsl:otherwise>red</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:template match="tst:element1">
<h2><font color="{$var_fc}"><xsl:value-of select="self::node()"/></font></h2>
</xsl:template>
As a result, element1 should turn gray, but it always turn red.
You can't use a variable for this, as the content of an xsl:variable is evaluated just once at definition time, whereas you want to evaluate some logic every time the variable is referenced, in the current context at the point of reference.
Instead you need a template, either a named one:
<xsl:template name="fc">
<xsl:choose>
<xsl:when test="local-name()='element1'">gray</xsl:when>
<xsl:otherwise>red</xsl:otherwise>
</xsl:choose>
</xsl:template>
or (better) a pair of matching templates with a mode, to let the template matcher do the work:
<!-- match any node whose local name is "element1" -->
<xsl:template mode="fc" match="node()[local-name() = 'element1']">gray</xsl:template>
<!-- match any other node -->
<xsl:template mode="fc" match="node()">red</xsl:template>
When you want to use this logic:
<h1>
<font>
<xsl:attribute name="color">
<xsl:apply-templates select="." mode="fc" />
</xsl:attribute>
Seeing as you have the tst prefix mapped in your stylesheet you could check the name directly instead of using the local-name() predicate:
<xsl:template mode="fc" match="tst:element1">gray</xsl:template>
<xsl:template mode="fc" match="node()">red</xsl:template>
XSLT variables are designed not to be changeable. Actually they could be named constants. If your variable fc is created global, it will use the root element for choose. You have to use choose in the actual template to be tested against the current element. If you want to have "red" and "gray" defined only once, create two variables with just that text content and use these instead the plain text in the choose.
Maybe it is a typo:
<xsl:when test=self::node()='element1'">gray</xsl:when>
should be:
<xsl:when test="self::node()='element1'">gray</xsl:when>
there is a missing quote.
I think instead of test="self::node()='element1'" you want test="self::element1" or test="local-name(.) = 'element1'".
A couple of other errors in your code:
(1) self::node() = 'element1'
tests whether the content of the element is "element1", not whether its name is "element1"
(2) local-name(.)='tst:element1'
will never be true because the local name of a node never contains a colon.
Experienced users would often write this code using template rules:
<xsl:template mode="var_fc" match="tst:element1">gray</xsl:template>
<xsl:template mode="var_fc" match="*">red</xsl:template>
and then
<xsl:apply-templates select="." mode="var_fc"/>

XSLT; Group 2 or more different elements BUT only when adjacent

Initially this seemed a trivial problem, but it seems to be harder than I thought.
In the following xml, I want to group adjacent 'note' and p elements only. A note should always start a notegroup and any following p should be included. No other elements are allowed in the group.
From this:
<doc>
<note />
<p/>
<p/>
<other/>
<p/>
<p/>
</doc>
To this:
<doc>
<notegroup>
<note />
<p/>
<p/>
</notegroup>
<other/>
<p/>
<p/>
</doc>
Seems ridiculously easy, but the rule is: 'note' and any following 'p'. Any p on their own are to be ignored (as in the last 2 p above)
With xslt 2.0, if I try something like:
<xsl:for-each-group select="*" group-adjacent="boolean(self::note) or boolean(self::p)">
fails because it also groups the two later p elements.
Or the 'starts-with' approach on the 'note' which seems to indiscriminately group any element after (instead of just the p elements).
The other approach I'm considering is to simply add an attribute to each note and the p that immediately follow the note, and using that to group later, but how can I do that?
Thanks for any answers
I think you can do this by being a bit creative with group-starting-with, essentially you want to start a new group whenever you see an element that is not one that belongs in a notegroup. In your example this would generate two groups - note+p+p and other+p+p, the trick is to only wrap a notegroup around groups where the initial item is a note, and to simply output groups that are not headed by a note unchanged
<xsl:for-each-group select="*" group-starting-with="*[not(self::p)]">
<xsl:choose>
<xsl:when test="self::note">
<notegroup>
<xsl:copy-of select="current-group()"/>
</notegroup>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
This will wrap all note elements in a notegroup even if they don't actually have any following p elements. If you don't want to wrap a "bare" note then make it <xsl:when test="self::note and current-group()[2]"> to trigger the wrapping only when the current group has more than one member.
If you have more than one element name that can be part of a notegroup then you could either list them all in the predicate
<xsl:for-each-group select="*" group-starting-with="*[not(self::p|self::ul)]">
or declare a variable holding the node names that can be part of a notegroup:
<xsl:variable name="notegroupMembers" select="(xs:QName('p'), xs:QName('ul'))" />
and then say
<xsl:for-each-group select="*"
group-starting-with="*[not(node-name(.) = $notegroupMembers)]">
taking advantage of the fact that an = comparison where one side is a sequence succeeds if any of the items in the sequence match.
Issue
You're group on either <note> or <p>. Hence the failing.
Hint
You can try by using group-starting-with="note",as describe in Grouping With XSLT 2.0 # XML.com:
<xsl:template match="doc">
<doc>
<xsl:for-each-group select="*" group-starting-with="note">
<notegroup>
<xsl:for-each select="current-group()">
<xsl:copy>
<xsl:apply-templates select="self::note or self::p" />
</xsl:copy>
</xsl:for-each>
</notegroup>
</xsl:for-each-group>
</doc>
</xsl:template>
You may need another <xsl:apply-templates /> around the end.

Name function in XPATH

I have the following elements in my xmi file:
<element1 id= 3 >
<element2 id= 3>
I want to transform them into something like:
<element1 id= 3 name =element2>
<element2 id= 3>
I am using xslt to transform:
<xsl:if test="#id = //*[#id]/#id">
<xsl:sequence
select="fn:createAtt('name',X)" />
</xsl:if>
I want to compare the id of two elements and in case they match then i want to save the name of second element (element2) into the name attribute of first element.
The comparison works ok. The problem is how to read the name of the second element ? I tried to use the name() function but am not able to read exactly that name that matches the comparison.
I would do it like this: first define a key as
<xsl:key name="el-by-id" match="*" use="#id"/>
then I would write a template
<xsl:template match="*[#id]">
<xsl:variable name="same-id" use="key('el-by-id', #id) except ."/>
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:if test="$same-id">
<xsl:attribute name="name" select="node-name($same-id[1])"/>
</xsl:if>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
That way you can efficiently reference the elements using a key, then if one element of the same id is found the attribute named name is created. I used the XSLT/XPath 2.0 node-name function, depending on your exact requirements you might want to use <xsl:attribute name="name" select="name($same-id[1])"/> instead.

XSLT for-each counter - how to access data

for performance testing purposes I want to take a small XML file and create a bigger one from it - using XSLT. Here I plan to take each entity (Campaign node in the example below) in the original XML and copy it n times, just changing its ID.
The only way I can think of to realize this, is a xsl:for-each select "1 to n". But when I do this I do not seem to be able to access the entity node anymore (xsl:for-each select="campaigns/campaign" does not work in my case). I am getting a processor error: "cannot be used here: the context item is an atomic value".
It seems that by using the "1 to n" loop, I am loosing the access to my actual entity. Is there any XPath expression that gets me access back or does anyone have a completely different idea how to realize this?
Here is what I do:
Original XML
<campaigns>
<campaign id="1" name="test">
<campaign id="2" name="another name">
</cmpaigns>
XSLT I try to use
<xsl:template match="/">
<xsl:element name="campaigns">
<xsl:for-each select="1 to 10">
<xsl:for-each select="campaigns/campaign">
<xsl:element name="campaign">
<xsl:copy-of select="#*[local-name() != 'id']" />
<xsl:attribute name="id"><xsl:value-of select="#id" /></xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:for-each>
</xsl:element>
</xsl:template>
Define a variable as the first thing in the match, like so:
<xsl:variable name="foo" select="."/>
This defines a variable $foo of type nodeset. Then access it like this
<xsl:for-each select="$foo/campaigns/campaign">
...
</xsl:for-each>

I need to build a string from a list of attributes (not elements) in XSLT

want to make a comma-delimited string from a list of 3 possible attributes of an element.
I have found a thread here:
XSLT concat string, remove last comma
that describes how to build a comma-delimited string from elements. I want to do the same thing with a list of attributes.
From the following element:
<myElement attr1="Don't report this one" attr2="value1" attr3="value2" attr4="value3" />
I would like to produce a string that reads: "value1,value2,value3"
One other caveat: attr2 thru attr4 may or may not have values but, if they do have values, they will go in order. So, attr4 will not have a value if attr3 does not. attr3 will not have a value if attr2 does not. So, for an attribute to have a value, the one before it in the attribute list must have a value.
How can I modify the code in the solution to the thread linked to above so that it is attribute-centric instead of element-centric?
Thanks in advance for whatever help you can provide.
This is easy in principe, but only if it is really clear which attribute you want to exclude. Since attributes are not by definition ordered in XML (in contrast the elements), you need to say how the attribute(s) to skip can be identified.
Edit: Regarding the attribute order, XML section 3.1 says:
Note that the order of attribute specifications in a start-tag or empty-element tag is not significant.
That said, something like this should do the trick (adjust the [] condition as you see fit):
<xsl:template match="myElement">
<xsl:for-each select="#*[position()!=1]">
<xsl:value-of select="." />
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
In XSLT 2.0 it is as easy as
<xsl:template match="myElement">
<xsl:value-of select="#* except #att1" separator=","/>
</xsl:template>
I would appreciate Lucero's answer .. He definitely has nailed it ..
Well, Here is one more code which truncates attribute attr1, which appears at any positions other than 1 in attribute list.
scenario like this::
<myElement attr2="value1" attr3="value2" attr4="value3" attr1="Don't report this one" />
Here is the XSLT code .. ::
<xsl:template match="myElement">
<xsl:for-each select="#*[name()!='attr1']">
<xsl:value-of select="." />
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
The output will be:
value1,value2,value3
If you wish to omit more than one attribute say .. attr1 and attr2 ..
<xsl:template match="myElement">
<xsl:for-each select="#*[name()!='attr1' and name()!='attr2']">
<xsl:value-of select="." />
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
The corresponding output will be:
value2,value3