XPath: Selecting a node based on another nodes value - xpath

I am trying to use a single XPath expression to select a node that has a child node which matches another node in the document.
A match would mean that ALL attributes of the node are the same. So if a node was being compared with several attributes doing individual attribute comparisons would be unmaintainable.
As an example given the following:
<Network>
<Machines>
<Machine Name = "MyMachine">
<Services>
<ServiceDetails Description="MyService" Executable="c:\Myservice.exe" DisplayName="My Service" Version="5"/>
</Services>
</Machine>
...
</Machines>
<Services>
<Service Name = "Service1">
<ServiceDetails Description="MyService" Executable="c:\Myservice.exe" DisplayName="My Service" Version="5"/>
</Service>
...
</Services>
</Network>
I want to get the service node from Services based on the ServiceDetails listed under MyMachine.
I thought it would look something like:
//Services/Service[ServiceDetails = //Machines/Machine[#Name='MyMachine']/ServiceDetails]
but it doesn't seem to work. I suspect the '=' operator isn't handling the node comparison correctly. I think there are some XPath 2.0 Methods that might work but I am using .NET 4.0 (System.XML namespace) I do not know if I can use them. If XPath 2.0 methods would help here I would really appreciate an explanation on how to use them in .Net 4.0.
Thanks

Use:
/*/Services/Service
[ServiceDetails/#Description
=
/*/Machines/Machine[#Name = "MyMachine"]
/Services/ServiceDetails/#Description
]

Try this will validate all attribute values are equal in both the elements then it is true:
/Network[(descendant::ServiceDetails/#Description = /Network//Machine[#Name = "MyMachine"]/Services/ServiceDetails/#Description) and (descendant::ServiceDetails/#Executable = /Network//Machine[#Name = "MyMachine"]/Services/ServiceDetails/#Executable) and (descendant::ServiceDetails/#DisplayName = /Network//Machine[#Name = "MyMachine"]/Services/ServiceDetails/#DisplayName) and (descendant::ServiceDetails/#Version = /Network//Machine[#Name = "MyMachine"]/Services/ServiceDetails/#Version)]

Related

Xpath sibling filter based on value of element in current node

Is there an Xpath to find a cousin node that has an element that matches the value of an element in the current node?
Please see below - I am iterating over each "Order" node and want to return the value of LocationID from the Collection node that has the same OrderLoadRef value as the order. For the first order it should return "AAA", for the second it should return "BBB".
The XPath works if I change the value of the OrderLoadRef manually, but how to I set it to be the value of the OrderLoadRef in the current Order Element? I've tried using the self axis, but think by the time we get to the condition, "self" is the collection node, not the order?
I can't hard code relative collection / order node positions as there could be a variable number of these nodes in the XML that my parser receives.
XDocument xDoc = XDocument.Parse(#"<DocRoot>
<Load>
<Collections>
<Collection>
<OrderLoadRef>1</OrderLoadRef>
<LocationID>AAA</LocationID>
</Collection>
<Collection>
<OrderLoadRef>2</OrderLoadRef>
<LocationID>BBB</LocationID>
</Collection>
</Collections>
<Orders>
<Order>
<OrderRef>1521505</OrderRef>
<OrderLoadRef>1</OrderLoadRef>
</Order>
<Order>
<OrderRef>1521505_2</OrderRef>
<OrderLoadRef>2</OrderLoadRef>
</Order>
</Orders>
</Load>
</DocRoot>");
List<XElement> orders = xDoc.XPathSelectElements("//Order").ToList();
foreach(XElement order in orders)
{
string locationId = order.XPathSelectElement("parent::Orders/parent::Load/Collections/Collection[OrderLoadRef = {OrderLoadRef from current order element}]/LocationID").Value;
}
Edited to add: I need this to be a purely XPath solution as I'm not able to alter the C# code in the parser. More than happy to be told it's not possible, but wanted to make sure before I relayed the message!
As Mads said, XPath 3 and later (i.e. the current version 3.1) allows you to use a let expression so e.g.
for $order in /DocRoot/Load/Orders/Order
return
let $col := /DocRoot/Load/Collections/Collection[OrderLoadRef = $order/OrderLoadRef]/LocationID
return $col
is pure XPath 3 and returns (for your sample) the two LocationID elements:
<LocationID>AAA</LocationID>
<LocationID>BBB</LocationID>
In the .NET framework XmlPrime and Saxon.NET support XPath 3.1 and XQuery 3.1 although only XmlPrime has extension methods for C# to work against XDocument, I think, Saxon.NET does allow XPath 3.1 against its XDM tree model or against System.Xml.XmlDocument.
XPath 3.0 (and greater) supports let expressions, which would allow you to do what you want. You could let a variable with the OrderLoadRef from the context node and use it within a predicate selecting the desired Collection by it's OrderLoadRef.
For a static XPath 1.0 expression, I don't think you can achieve what you want. You would need to construct the XPath using the context node information.
Inside your for loop, create a variable for the Order's OrderLoadRef value. Use that value to construct the XPath that you want to evaluate to then select the locationId
foreach(XElement order in orders)
{
string orderLoadRef = order.XPathSelectElement("OrderLoadRef").Value;
string locationId = order.XPathSelectElement("ancestor::Load/Collections/Collection[OrderLoadRef = " + orderLoadRef + "]/LocationID").Value;
//do something with the locationId
}

How to refer to another instance in the iterate of the XForms action element?

I am using an XForms action along with iterate. The iterate selects a set (using XPath) of nodes and repeats the action for it.The problem is I have multiple conditions for selecting the node set.
There should not be a readOnly node.
Should not be part of the ignoreProperties list (this list is in another instance).
Code:
<xf:action ev:event="setValues" iterate="
instance('allProps')/props/prop[
not(readOnly) and
not(instance('ignoreProperties')/ignoredProperties/property[text() = name]
]
">
The first condition not(readOnly) works. But the second condition does not work. I feel there is some problem with the context of the XPath nodes.
How should I replace the second condition to achieve the result ?
The target XML is a simple ignoredProperties document:
<ignoredProperties>
<property>c_name</property>
<property>c_tel_no</property>
</ignoredProperties>
This should work:
<xf:action ev:event="setValues" iterate="
instance('allProps')/props/prop[
not(readOnly) and
not(name = instance('ignoreProperties')/ignoredProperties/property)
]
">
The = operator works against multiple nodes, returning all the ones that match. With not() you can express that you don't want a match.
Explicitly selecting .../property/text() will not be necessary.
There seems to be something wrong with your calls to instance(). If you have:
<xf:instance id="ignoredProperties">
<ignoredProperties>
<property>c_name</property>
<property>c_tel_no</property>
</ignoredProperties>
</xf:instance>
Then instance('ignoredProperties') returns the <ignoredProperties> element. So you should write:
<xf:action ev:event="setValues" iterate="
instance('allProps')/prop[
not(readOnly) and
not(instance('ignoreProperties')/property[text() = name])
]
">
This also assumes your allProps instance has a <props> root element.
Further, the second condition appears wrong, as already shown in another answer. Write instead:
not(name = instance('ignoreProperties')/property)
In XPath 2, you could clarify that your not() are testing on node existence by using empty() instead:
<xf:action ev:event="setValues" iterate="
instance('allProps')/prop[
empty(readOnly) and
not(name = instance('ignoreProperties')/property)
]
">

How to load the xml file from webpage and read particular nodes from xml?

I am planning to load below mentioned xml from the webpage and then want to read particular nodes from it.Filtering condition: if "displayname" attribute contains "isc-asr901a"it should pick the first node and return the attribute "id" value of node ethernetProtocolEndpointExtendedDTO"
<queryResponse type="EthernetProtocolEndpoint">
<entity >
<ethernetProtocolEndpointExtendedDTO id="2283315" displayName="4c2b8aa7[2275273_isc- asr901a,GigabitEthernet0/0]">
<name>GigabitEthernet0/0</name>
<adminStatus>UP</adminStatus>
</ethernetProtocolEndpointExtendedDTO>
</entity>
<entity >
<ethernetProtocolEndpointExtendedDTO id="2283315" displayName="4c2b8aa7[2275273_isc-asr901a,GigabitEthernet0/0]">
<name>GigabitEthernet0/0</name>
<adminStatus>UP</adminStatus>
</ethernetProtocolEndpointExtendedDTO>
</entity>
</queryResponse>
I am planning to do this using ruby. but I am new to ruby. Could someone help me to perform this. by using which parser i can do it easily? I am using below code to perform this but code is not returning any value.
strurl = "https://.."
doc = Nokogiri::HTML(open(strurl))
doc.xpath('//queryResponse/entity/ethernetProtocolEndpointDTO[#displayName="[^"]*isc-asr901a[^"]*]').each do |node|
puts node['id']
end
Thanks,
Chandana
You need to use Nokogiri::XML, not Nokogiri::HTML, since this is an XML. Furthermore, you had a typo in ethernetProtocolEndpointExtendedDTO - you wrote ethernetProtocolEndpointDTO.
Also, you should use contains to find the display names which contain your string:
strurl = "https://.."
doc = Nokogiri::XML(open(strurl))
doc.xpath('//queryResponse/entity/ethernetProtocolEndpointExtendedDTO[contains(#displayName, "isc-asr901a")]').each do |node|
puts node['id']
end
# => 2283315

Does xpath query has Limit option like mysql

I want to limit number of result I receive from xpath query.
For example:-
$info = $xml->xpath("//*[firstname='Sheila'] **LIMIT 0,100**");
You see that LIMIT 0,100.
You should be able to use "//*[firstname='Sheila' and position() <= 100]"
Edit:
Given the following XML:
<root>
<country.php desc="country.php" language="fr|pt|en|in" editable="Yes">
<en/>
<in>
<cityList desc="cityList" language="in" editable="Yes" type="Array" index="No">
<element0>Abu</element0>
<element1>Agartala</element1>
<element2>Agra</element2>
<element3>Ahmedabad</element3>
<element4> Ahmednagar</element4>
<element5>Aizwal</element5>
<element150>abcd</element150>
</cityList>
</in>
</country.php>
</root>
You can use the following XPath to get the first three cities:
//cityList/*[position()<=3]
Results:
Node element0 Abu
Node element1 Agartala
Node element2 Agra
If you want to limit this to nodes that start with element:
//cityList/*[substring(name(), 1, 7) = 'element' and position()<=3]
Note that this latter example works because you're selecting all the child nodes of cityList, so in this case Position() works to limit the results as expected. If there was a mix of other node names under the cityList node, you'd get undesirable results.
For example, changing the XML as follows:
<root>
<country.php desc="country.php" language="fr|pt|en|in" editable="Yes">
<en/>
<in>
<cityList desc="cityList" language="in" editable="Yes" type="Array" index="No">
<element0>Abu</element0>
<dog>Agartala</dog>
<cat>Agra</cat>
<element3>Ahmedabad</element3>
<element4> Ahmednagar</element4>
<element5>Aizwal</element5>
<element150>abcd</element150>
</cityList>
</in>
</country.php>
</root>
and using the above XPath expression, we now get
Node element0 Abu
Note that we're losing the second and third results, because the position() function is evaluating at a higher order of precedence - the same as requesting "give me the first three nodes, now out of those give me all the nodes that start with 'element'".
Ran into the same issue myself and had some issue with Geoffs answer as it, as he clearly describes, limits the number of elements returned before it performs the other parts of the query due to precedence.
My solution is to add the position() < 10 as an additional conditional after my other conditions have been applied e.g.:
//ElementsIWant[./ChildElementToFilterOn='ValueToSearchFor'][position() <= 10]/.
Notice that I'm using two separate conditional blocks.
This will first filter out elements that live up to my condition and secondly only take 10 of those.

Parsing XML with REXML

I have this XML document and I want to find an specific GitHubCommiter using REXML. Hoy do I do that?
<users>
<GitHubCommiter id="Nerian">
<username>name</username>
<password>12345</password>
</GitHubCommiter>
<GitHubCommiter id="xmawet">
<username>name</username>
<password>12345</password>
</GitHubCommiter>
<GitHubCommiter id="JulienChristophe">
<username>name</username>
<password>12345</password>
</GitHubCommiter>
</users>
I have tried:
log = REXML::Document.new(file)
root = log.root username = root.elements["GitHubCommiter['#{github_user_name}']"].elements['username'].text
password = root.elements["GitHubCommiter['#{github_user_name}']"].elements['password'].text
root.elements["GitHubCommiter['id'=>'#{github_user_name}']"].text
But I don't find a way to do it. Any idea?
The docs say for elements (emphasis mine):
[]( index, name=nil)
Fetches a child element. Filters only Element children, regardless of the XPath match.
index: the search parameter. This is either an Integer, which will be used to find the index‘th child Element, or an XPath, which will be used to search for the Element.
So it needs to be XPath:
root.elements["./GitHubCommiter[#id = '{github_user_name}']"]
etc.

Resources