How can I shorten this xpath expression? - xpath

I have defined a variable called tags which selects a node set of elements whose names are one of several values:
<xsl:variable name="tags" select="//xs:element[#name='Subscription' or #name='Account' or #name='Product' or #name='ProductRatePlan' or #name='ProductRatePlanCharge' or #name='Usage']"/>
I would like to define a variable as such:
<xsl:variable name="tagNames" select="'Subscription','Account','Product','ProductRatePlan','ProductRatePlanCharge','Usage'/>
How could I rewrite the first expression to select all the nodes whose names are in the set $tagNames? In essence, I'm looking for an operation that is analagous to a SQL set membership:
SELECT * FROM tags WHERE name in ('Subscription', 'Account', 'Product'....)

In XPath 2.0 you can write:
<xsl:variable name="tags" select="//xs:element[#name=('Subscription','Account','Product','ProductRatePlan','ProductRatePlanCharge','Usage')]"/>

One way to do this:
//xs:element
[contains('|Subscription|Account|Product|ProductRatePlan|ProductRatePlanCharge|Usage|',
concat('|', #name, '|'))]
So, you can have a variable or parameter:
<xsl:variable name="vTags"
select="'|Subscription|Account|Product|ProductRatePlan|ProductRatePlanCharge|Usage|'"/>
and your main XPath expression becomes:
//xs:element[contains($vTags, concat('|', #name, '|'))]

Related

Breaking for-each loop on a conditional base

I'm new to xslt 2.0, I would like to set the value to a variable in for-each loop only once (means if the value set, I want to come out of the loop).
For now it keep iterating for all the users. I just want to come out of the loop once the value set (immediately after my first attemp). I'm not sure how to break if the value set once.
Can you please help me on the below code ?
XSLT Code:
<xsl:variable name="v_first_name">
<xsl:for-each select="$emailList/emails/child::*">
<xsl:variable name="mailid" select="id" />
<xsl:for-each select="$userList/users/child::*">
<xsl:if test="emailid = $mailid">
<xsl:if test="firstname eq 'Antony'">
<xsl:value-of select="firstname" />
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:variable>
<xsl:if test="$v_first_name != ''">
<first_name>
<xsl:value-of select="$v_first_name" />
</first_name>
</xsl:if>
XML O/p:
<first_name>AntonyAntonyAntonyAntony</first_name>
Expected XML O/P:
<first_name>Antony</first_name>
Note1: Please note that I'm using xslt 2.0 and my lists can have duplicates (So Antony can come twice, but I want only once (or unique)).
Note2: I also tried with position(), but couldn't find it work as the condition () can match at any position.
Thanks in advance.
Start with XPath and simply select the nodes you are looking for instead of considering for-each a "loop". If you select e.g. $userList/users/*[emailid = $emailList/emails/*/id] you select child elements from users which have a matching emailid in $emailList/emails/*.
I am not sure which sense it makes to hard code a first name value and then output it but of course you can select e.g. $userList/users/*[emailid = $emailList/emails/*/id and firstname = 'Antony']/lastname. That gives you a sequence of element nodes, if you want the first use a positional predicate e.g. depending on the structure of your input $userList/users/*[emailid = $emailList/emails/*/id and firstname = 'Antony'][1]/lastname or, of all selected elements ($userList/users/*[emailid = $emailList/emails/*/id and firstname = 'Antony']/lastname)[1].

Using Variable in Node Attribute

I am writing a XSL template. I am not getting how to specify a variable for a custom attribute inside XSL file.
I am trying this code in XSL:
<xsl:variable name="var1" select="DEF"/>
<frequency myAttr="ABC"+$var1 >
<xsl:value-of select="frequency"/>
</frequency>
Expected result is
<frequency myAttr="ABCDEF" >20</frequency>
I am getting this error:
Unable to generate the XML document using the provided XML/XSL input. org.xml.sax.SAXParseException; lineNumber: 18; columnNumber: 24; Element type "sourceId" must be followed by either attribute specifications, ">" or "/>"
The issue is the way I am concatenating is wrong. Any help in achieving this?
I think you mean :
<frequency myAttr="ABC{$var1}" >
This will concatenate the literal string "ABC" with the content of the $var1 variable - see: Attribute Value Templates.
Note that the way you populate the variable suggests there is an element named DEF in the source XML. The expected result will be obtained only if this element has a string value of "DEF".
XSL is a declarative language in XML format. An XSLT file must be well-formed XML.
What you tried looks like you have programming knowledge of an imperative language where you can write expressions like "ABC" + $var1. This violates the XML format rules in XSL however. Elements follow the pattern <elementname attribute1="value1" attributeN="valueN">...</elementname>. In your code, you put +$var1 after the end of the myAttr attribute, which ends at the second quote mark - and this is invalid: <frequency myAttr="ABC"+$var1 >
<frequency myAttr="ABC+$var1"> would result in a literal attribute value of ABC+$var1, which is not what you want. This would also happen if you tried to use XPath syntax as attribute value, concat('ABC', $var1) as string from <frequency myAttr="concat('ABC', $var1)">.
You can use the Attribute Value Template syntax as michael.hor257k suggested, which essentially means to wrap XPath expressions in curly braces inside of attribute value strings.
Another way would be to not write the element as literal in your code, but rather declare it:
<xsl:variable name="var1" select="'DEF'"/>
<xsl:element name="frequency">
<xsl:attribute name="myAttr">
<xsl:value-of select="concat('ABC', $var1)"/>
</xsl:attribute>
</xsl:element>
Note that I corrected the variable definition: In the select attribute, you must put DEF in single quote marks if this is supposed to be a string, like select="'DEF'". Without the single quote marks you define the variable to refer to <DEF> elements in the source XML.
In the select attribute of <xsl:value-of> I used the XPath function concat() to concatenate the string 'ABC' and the content of the variable $var1. The attribute value template syntax can not be used here: select="'ABC{$var1}'" would result in the string ABC{$var1}.
A variation of above example would be to use <xsl:text> for the string ABC and <xsl:value-of> to output the content of $var1:
<xsl:variable name="var1" select="'DEF'"/>
<xsl:element name="frequency">
<xsl:attribute name="myAttr">
<xsl:text>ABC</xsl:text>
<xsl:value-of select="$var1"/>
</xsl:attribute>
</xsl:element>
So there are three solutions, a concise one and two declarative ones. Which one you choose is up to you. The quickest to type is certainly the first:
<xsl:variable name="var1" select="'DEF'"/>
<frequency myAttr="ABC{$var1}">
<xsl:value-of select="frequency"/>
</frequency>
... but you should also be aware of both declarative ways to achieve the same result and understand how they work.

Check attribute value of XML element with a regular expression pattern

I have this code, which does not work.
I want to iterate trough all p elements below the body element and look for an element named object. Then use the match() function to look for a certain pattern in the attribute node data.
<xsl:template match="/xhtml:html/xhtml:body//xhtml:p">
<xsl:for-each select="xhtml:object[matches(#data,'*mov$')]">
<halloMovie><xsl:value-of select="#data"/></halloMovie>
</xsl:for-each>
</xsl:template>
Error message while executing with matches()
[xslt] : Fatal Error! Could not compile stylesheet Cause: Error checking type of the expression 'funcall(matches, [step("attribute", 17), literal-expr(v$)])'.
If I use the following line with the contains() function which works.
<xsl:for-each select="xhtml:object[contains(#data,'.mov')]">
What am I doing wrong?
The matches function requires XPath 2.0 (and hence XSLT 2.0) or later, there's no regular expression support in XSLT 1.0.
If you just want to check that the value ends with ".mov" you can do that using the substring function
substring(#data, string-length(#data) - 3) = '.mov'
(XPath 1.0 has starts-with but you need 2.0 for ends-with).

Sorting XPath results in the same order as multiple select parameters

I have an XML document as follows:
<objects>
<object uid="0" />
<object uid="1" />
<object uid="2" />
</objects>
I can select multiple elements using the following query:
doc.xpath("//object[#uid=2 or #uid=0 or #uid=1]")
But this returns the elements in the same order they're declared in the XML document (uid=0, uid=1, uid=2) and I want the results in the same order as I perform the XPath query (uid=2, uid=0, uid=1).
I'm unsure if this is possible with XPath alone, and have looked into XSLT sorting, but I haven't found an example that explains how I could achieve this.
I'm working in Ruby with the Nokogiri library.
There is no way in XPath 1.0 to specify the order of the selected nodes.
XPath 2.0 allows a sequence of nodes with any specific order:
//object[#uid=2], //object[#uid=1]
evaluates to a sequence in which all object items with #uid=2 precede all object items with #uid=1
If one doesn't have anXPath 2.0 engine available, it is still possible to use XSLT in order to output nodes in any desired order.
In this specific case the sequence of the following XSLT instructions:
<xsl:copy-of select="//object[#uid=2]"/>
<xsl:copy-of select="//object[#uid=1]"/>
produces the desired output:
<object uid="2" /><object uid="1" />
I am assuming you are using XPath 1.0. The W3C spec says:
The primary syntactic construct in XPath is the expression. An expression matches the production Expr. An expression is evaluated to yield an object, which has one of the following four basic types:
* node-set (an unordered collection of nodes without duplicates)
* boolean (true or false)
* number (a floating-point number)
* string (a sequence of UCS characters)
So I don't think you can re-order simply using XPath. (The rest of the spec defines document order and reverse document order, so if the latter does what you want you can get it using the appropriate axis (e.g. preceding).
In XSLT you can use <xsl:sort> using the name() of the attribute. The XSLT FAQ is very good and you should find an answer there.
An XSLT example:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="pSequence" select="'2 1'"/>
<xsl:template match="objects">
<xsl:for-each select="object[contains(concat(' ',$pSequence,' '),
concat(' ',#uid,' '))]">
<xsl:sort select="substring-before(concat(' ',$pSequence,' '),
concat(' ',#uid,' '))"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Output:
<object uid="2" /><object uid="1" />
I don't think there is a way to do it in xpath but if you wish to switch to XSLT you can use the xsl:sort tag:
<xsl:for-each select="//object[#uid=1 or #uid=2]">
<xsl:sort: select="#uid" data-type="number" />
{insert new logic here}
</xsl:for-each>
more complete info here:
http://www.w3schools.com/xsl/el_sort.asp
This is how I'd do it in Nokogiri:
require 'nokogiri'
xml = '<objects><object uid="0" /><object uid="1" /><object uid="2" /></objects>'
doc = Nokogiri::XML(xml)
objects_by_uid = doc.search('//object[#uid="2" or #uid="1"]').sort_by { |n| n['uid'].to_i }.reverse
puts objects_by_uid
Running that outputs:
<object uid="2"/>
<object uid="1"/>
An alternative to the search would be:
objects_by_uid = doc.search('//object[#uid="2" or #uid="1"]').sort { |a,b| b['uid'].to_i <=> a['uid'].to_i }
if you don't like using sort_by with the reverse.
XPath is useful for locating and retrieving the nodes but often the filtering we want to do gets too convoluted in the accessor so I let the language do it, whether it's Ruby, Perl or Python. Where I put the filtering logic is based on how big the XML data set is and whether there are a lot of different uid values I'll want to grab. Sometimes letting the XPath engine do the heavy lifting makes sense, other times its easier to let XPath grab all the object nodes and filter in the calling language.

XPath 1 query and attributes name

First question: is there any way to get the name of a node's attributes?
<node attribute1="value1" attribute2="value2" />
Second question: is there a way to get attributes and values as value pairs? The situation is the following:
<node attribute1="10" attribute2="0" />
I want to get all attributes where value>0 and this way: "attribute1=10".
First question: is there any way to
get the name of a node's attributes?
<node attribute1="value1"
attribute2="value2" />
Yes:
This XPath expression (when node is the context (current) node)):
name(#*[1])
produces the name of the first attribute (the ordering may be implementation - dependent)
and this XPath expression (when node is the context (current) node)):
name(#*[2])
produces the name of the second attribute (the ordering may be implementation - dependent).
Second question: is there a way to get
attributes and values as value pairs?
The situation is the following:
<node attribute1="10" attribute2="0"
/>
I want to get all attributes where
value>0 and this way: "attribute1=10".
This XPath expression (when the attribute named "attribute1" is the context (current) node)):
concat(name(), '=', .)
produces the string:
attribute1=value1
and this XPath expression (when the node node is the context (current) node)):
#*[. > 0]
selects all attributes of the context node, whose value is a number, greater than 0.
In XPath 2.0 one can combine them in a single XPath expression:
#*[number(.) > 0]/concat(name(.),'=',.)
to get (in this particular case) this result:
attribute1=10
If you are using XPath 1.0, which is less powerful, you'll need to embed the XPath expression in a hosting language, such as XSLT. The following XSLT 1.0 thransformation :
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:for-each select="#*[number(.) > 0]">
<xsl:value-of select="concat(name(.),'=',.)"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document:
<node attribute1="10" attribute2="0" />
Produces exactly the same result:
attribute1=10
It depends a little bit on the context, I believe. In most cases, I expect you'd have to query "#*", enumerate over the items, and call "name()" - but it may work in some tests.
Re the edit - you can do:
#*[number(.)>0]
to find attributes matching your criteria, and:
concat(name(),'=',.)
to display the output. I don't think you can do both at once, though. What is the context here? xslt? what?

Resources