Selecting a XML node with LINQ, and modifying - linq

I've got the following XML:
<Config>
<Book>
<Name> Book Name #1 </Name>
<Available In>
<Country>US</Country>
<Country>Canada</Country>
</Available In>
</Book>
</Config>
I need to find all instances of Book which are available in a specific country, and then introduce a node underneath "Available In". My selection statement fails anytime I add the where statement:
XElement xmlFile = XElement.Load(xmlFileLocation);
var q = (from c in xmlFile.Elements(“Book”)
where c.Elements(Country).Value == "Canada"
select c;
.Value can't be resolved, and toString give me the entire subnode in stringform. I need to select all books in a particular country so that I can then update them all to include a new locale node, ex:
<Config>
<Book>
<Name> Book Name #1 </Name>
<Available In>
<Country>US</Country>
<Country>Canada</Country>
</Available In>
<LocaleIDs>
<LocalID> 3066 </LocaleID>
<LocaleIDs>
</Book>
</Config>
Thanks for your help!

You're trying to use Value on the result of calling Elements which returns a sequence of elements. That's not going to work - it doesn't make any sense. You want to call it on a single element at a time.
Additionally, you're trying to look for direct children of Book, which ignores the Available In element, which isn't even a valid element name...
I suspect you want something like:
var query = xmlFile.Elements("Book")
.Where(x => x.Descendants("Country")
.Any(x => (string) x == "Canada"));
In other words, find Book elements where any of the descendant Country elements has a text value of "Canada".
You'll still need to fix your XML to use valid element names though...

Related

SchemaTron rule to find invalid records

I am trying to validate the following XML using the Schematron rule.
XML:
<?xml version="1.0" encoding="utf-8"?>
<Biotic><Maul><Number>1</Number>
<Record><Code IDREF="a1"/>
<Detail><ItemID>1</ItemID></Detail>
<Detail><ItemID>3</ItemID></Detail>
</Record>
<Record><Code IDREF="b1"/>
<Detail><ItemID>3</ItemID></Detail>
<Detail><ItemID>4</ItemID></Detail>
</Record>
<Record><Code IDREF="b1"/>
<Detail><ItemID>4</ItemID></Detail>
<Detail><ItemID>6</ItemID></Detail>
</Record>
<Record><Code IDREF="c1"/>
<Detail><ItemID>5</ItemID></Detail>
<Detail><ItemID>5</ItemID></Detail>
</Record>
</Maul></Biotic>
And the check is "ItemID should be unique for the given Code within the given Maul."
So as per requirement Records with Code b1 is not valid because ItemId 4 exists in both records.
Similarly, record C1 is also not valid because c1 have two nodes with itemId 5.
Record a1 is valid, even ItemID 3 exists in the next record but the code is different.
Schematron rule I tried:
<?xml version="1.0" encoding="utf-8" ?><schema xmlns="http://purl.oclc.org/dsdl/schematron" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<title>Schematron validation rule</title>
<pattern id="P1">
<rule context="Maul/Record" id="R1">
<let name="a" value="//Detail/[./ItemID, ../Code/#IDREF]"/>
<let name="b" value="current()/Detail/[./ItemID, ../Code/#IDREF]"/>
<assert test="count($a[. = $b]) = count($b)">
ItemID should be unique for the given Code within the given Maul.
</assert>
</rule>
</pattern>
</schema>
The two let values seem problematic. They will each return a Detail element (and all of its content including attributes, child elements, and text nodes). I'm not sure what the code inside the predicates [./ItemID, ../Code/#IDREF] is going to, but I think it will return all Detail elements that have either a child ItemID element or a sibling Code element with an #IDREF attribute, regardless of what the values of ItemID or #IDREF are.
I think I would change the rule/#context to ItemID, so the assert would fail once for each ItemID that violates the constraint.
Here are a rule and assert that work correctly:
<?xml version="1.0" encoding="utf-8" ?><schema xmlns="http://purl.oclc.org/dsdl/schematron" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<title>Schematron validation rule</title>
<pattern id="P1">
<rule context="Maul/Record/Detail/ItemID" id="R1">
<assert test="count(ancestor::Maul/Record[Code/#IDREF = current()/ancestor::Record/Code/#IDREF]/Detail/ItemID[. = current()]) = 1">
ItemID should be unique for the given Code within the given Maul.
</assert>
</rule>
</pattern>
</schema>
The assert test finds, within the ancestor Maul, any Record that has a Code/#IDREF that equals the Code/#IDREF of the Record that the current ItemID is in. At minimum, it will find one Record (the one that the current ItemID is in). Then it looks for any Detail/ItemID within those Records that is equal to the current ItemID. It will find at least one (the current ItemID). The count function counts how many ItemIDs are found. If more than one is found, the assert fails.
Thanks for the reference to https://www.liquid-technologies.com/online-schematron-validator! I wasn't aware of that tool.

How to find the parent node by matching text using XPath

I have some XML:
<sys>
<lang>
<employee>
<name>Employee 1</name>
<code>4fdaa994-7015-4ec1-b365-de4ee0279966</code>
</employee>
<employee>
<name>Employee 2</name>
<code>1d960bdc-0853-49af-bb83-18cf92493897</code>
</employee>
</lang>
</syz>
How can I search and get the employee node where name ="Employee 1"?
I tried this but it didn't work:
obj.xpath("//sys/lang[/employee/name = 'Employee 1']")
This XPath
/sys/lang/employee[name = 'Employee 1']
will select the employee element whose name is Employee 1.
Why might OP be getting an "Invalid expression" using the above XPath?
Transcription error.
Resolution: Use copy and paste.
Single quotes around single quotes.
Resolution: Use outer double quotes: "/sys/lang/employee[name = 'Employee 1']"
Smart quotes.
Resolution: Replace ‘ and ’ with single quote '.
Misinterpretation of error message.
Resolution: Carefully check any line number mentioned in error, or carve away surrounding code as much as possible, and see if error goes away.
If none of the above possibilities apply, post a MCVE (Minimal, Complete, and Verifiable Example, including the provided XPath and the calling code -- the complete in MCVE) that produces the invalid expression error, and someone will likely immediately spot the problem.
I'm a big fan of using CSS over XPath for readability reasons. Nokogiri implements a number of jQuery's extensions to make it easier to use CSS for things we'd usually use XPath for.
I'd do it this way:
require 'nokogiri'
doc = Nokogiri::XML(<<EOT)
<sys>
<lang>
<employee>
<name>Employee 1</name>
<code>4fdaa994-7015-4ec1-b365-de4ee0279966</code>
</employee>
<employee>
<name>Employee 2</name>
<code>1d960bdc-0853-49af-bb83-18cf92493897</code>
</employee>
</lang>
</syz>
EOT
emp1 = doc.at('employee name:contains("Employee 1")') # => #<Nokogiri::XML::Element:0x3ffed05285b4 name="name" children=[#<Nokogiri::XML::Text:0x3ffed05283d4 "Employee 1">]>
emp1.to_xml # => "<name>Employee 1</name>"
emp1.parent.to_xml # => "<employee>\n <name>Employee 1</name>\n <code>4fdaa994-7015-4ec1-b365-de4ee0279966</code>\n </employee>"
Also note, it's not good practice to define the full path in the selector for a node. If the HTML or XML changes the structure that selector will break. Instead, find useful landmarks and hop from one to the next. That way your selector is more likely to survive changes in the markup. I only care about finding the appropriate <employee>...<name> combination, not those two tags embedded under <sys> and <lang>.
Sometimes an alternate way of getting to the information you want is to use search and look at a particular index:
doc.search('employee').first.to_xml # => "<employee>\n <name>Employee 1</name>\n <code>4fdaa994-7015-4ec1-b365-de4ee0279966</code>\n </employee>"
Or:
doc.at('employee').to_xml # => "<employee>\n <name>Employee 1</name>\n <code>4fdaa994-7015-4ec1-b365-de4ee0279966</code>\n </employee>"
at('some selector') is equivalent to search('some selector').first.

Get low level xpath from XML with Nokogiri

I'm trying to store in an array all the unique Xpaths of the low level elements in the XML below, but like I'm doing in array a is being stored all the XML, not only the Xpath themselves. The XML has different levels of Xpath. I mean, some child elements only have 2 ancestors and others more than one.
This is the code I have.
require 'nokogiri'
doc = Nokogiri::XML(<<EOT)
<?xml version="1.0" encoding="UTF-8"?>
<items>
<item>
<name>Cake</name>
<ppu>0.55</ppu>
<batters>
<batter>Regular</batter>
<batter>Chocolate</batter>
<batter>Blueberry</batter>
<batter>Devil's Food</batter>
</batters>
<topping>None</topping>
<topping>Glazed</topping>
<topping>Sugar</topping>
<topping>Powdered Sugar</topping>
<topping>Chocolate with Sprinkles</topping>
<topping>Chocolate</topping>
<topping>Maple</topping>
</item>
<item>
<name>Raised</name>
<ppu>0.55</ppu>
<batters>
<batter>Regular</batter>
</batters>
<topping>None</topping>
<topping>Glazed</topping>
<topping>Sugar</topping>
<topping>Chocolate</topping>
<topping>Maple</topping>
</item>
</items>
EOT
a = []
a = doc.xpath("//*")
puts a
I'd like to store in array "a" only the unique xpaths as below:
/items/item/name
/items/item/ppu
/items/item/batters/batter
/items/item/topping
Maybe somebody could help me in how to do this.
Thanks for the help.
What you want to select is the "leaf" nodes. You can do it like so:
doc.xpath("//*[not(*)]")
This means "select all elements that don't contain elements".
If you want the XPaths, you'll need to call .path on each node. But the paths provided by Nokogiri have explicit positions (e.g. /items/item[2]/topping[4]), so you'll have to apply a regex to remove them, then remove duplicates with uniq:
doc.xpath("//*[not(*)]").map {|leaf| leaf.path.gsub(/\[.*?\]/, '') }.uniq
Output:
/items/item/name
/items/item/ppu
/items/item/batters/batter
/items/item/topping

XPath Expression referencing a node

I am trying to reference a node in an expression. Take this simple example:
<?xml version="1.0" encoding="UTF-8" ?>
<homelist>
<homes>
<home>
<hname>house</hname>
<location>hell</location>
<url>wee</url>
<cID>1234</cID>
</home>
</homes>
<contacts>
<contactdetails cID="1234">
<cname>John Smith</cname>
<phone>0123234</phone>
<email>test#gmail.com</email>
</contactdetails>
</contacts>
</homelist>
I basically want to select nodes if it's value is somewhere else in the tree.
For example, I want to display the url of homes that have cID of John Smith. I tried this but it doesn't work, what is wrong with it:
homelist/homes/home[ancestor::homelist/contacts/contactdetails[cname="John Smith"]/url
"/homelist/homes/home[cID = /homelist/contacts/contactdetails[cname='John Smith']/#cID]/url"
You want to find the <home> whose <cID> child's text content equals that of the cID= attribute of the <contactdetails> whose <cname> contains 'John Smith', then return its <url> child.
Note that I've written this as an absolute path, from the root, since you didn't tell us what the context node was going to be for this XPath.
There are certainly other ways of writing the same concept; this is just the first one that occurred to me offhand.
If you preferred to use ancestor or parent, you could say
"/homelist/homes/home[cID = ancestor::homelist/contacts/contactdetails[cname='John Smith']/#cID]/url"

How to select the first element with a specific attribute using XPath

The XPath bookstore/book[1] selects the first book node under bookstore.
How can I select the first node that matches a more complicated condition, e.g. the first node that matches /bookstore/book[#location='US']
Use:
(/bookstore/book[#location='US'])[1]
This will first get the book elements with the location attribute equal to 'US'. Then it will select the first node from that set. Note the use of parentheses, which are required by some implementations.
Note, this is not the same as /bookstore/book[1][#location='US'] unless the first element also happens to have that location attribute.
/bookstore/book[#location='US'][1] works only with simple structure.
Add a bit more structure and things break.
With-
<bookstore>
<category>
<book location="US">A1</book>
<book location="FIN">A2</book>
</category>
<category>
<book location="FIN">B1</book>
<book location="US">B2</book>
</category>
</bookstore>
/bookstore/category/book[#location='US'][1] yields
<book location="US">A1</book>
<book location="US">B2</book>
not "the first node that matches a more complicated condition". /bookstore/category/book[#location='US'][2] returns nothing.
With parentheses you can get the result the original question was for:
(/bookstore/category/book[#location='US'])[1] gives
<book location="US">A1</book>
and (/bookstore/category/book[#location='US'])[2] works as expected.
As an explanation to Jonathan Fingland's answer:
multiple conditions in the same predicate ([position()=1 and #location='US']) must be true as a whole
multiple conditions in consecutive predicates ([position()=1][#location='US']) must be true one after another
this implies that [position()=1][#location='US'] != [#location='US'][position()=1]
while [position()=1 and #location='US'] == [#location='US' and position()=1]
hint: a lone [position()=1] can be abbreviated to [1]
You can build complex expressions in predicates with the Boolean operators "and" and "or", and with the Boolean XPath functions not(), true() and false(). Plus you can wrap sub-expressions in parentheses.
The easiest way to find first english book node (in the whole document), taking under consideration more complicated structered xml file, like:
<bookstore>
<category>
<book location="US">A1</book>
<book location="FIN">A2</book>
</category>
<category>
<book location="FIN">B1</book>
<book location="US">B2</book>
</category>
</bookstore>
is xpath expression:
/descendant::book[#location='US'][1]
<bookstore>
<book location="US">A1</book>
<category>
<book location="US">B1</book>
<book location="FIN">B2</book>
</category>
<section>
<book location="FIN">C1</book>
<book location="US">C2</book>
</section>
</bookstore>
So Given the above; you can select the first book with
(//book[#location='US'])[1]
And this will find the first one anywhere that has a location US. [A1]
//book[#location='US']
Would return the node set with all books with location US. [A1,B1,C2]
(//category/book[#location='US'])[1]
Would return the first book location US that exists in a category anywhere in the document. [B1]
(/bookstore//book[#location='US'])[1]
will return the first book with location US that exists anywhere under the root element bookstore; making the /bookstore part redundant really. [A1]
In direct answer:
/bookstore/book[#location='US'][1]
Will return you the first node for book element with location US that is under bookstore [A1]
Incidentally if you wanted, in this example to find the first US book that was not a direct child of bookstore:
(/bookstore/*//book[#location='US'])[1]
Use the index to get desired node if xpath is complicated or more than one node present with same xpath.
Ex :
(//bookstore[#location = 'US'])[index]
You can give the number which node you want.
if namespace is provided on the given xml, its better to use this.
(/*[local-name() ='bookstore']/*[local-name()='book'][#location='US'])[1]
for ex.
<input b="demo">
And
(input[#b='demo'])[1]
With help of an online xpath tester I'm writing this answer...
For this:
<table id="t2"><tbody>
<tr><td>123</td><td>other</td></tr>
<tr><td>foo</td><td>columns</td></tr>
<tr><td>bar</td><td>are</td></tr>
<tr><td>xyz</td><td>ignored</td></tr>
</tbody></table>
the following xpath:
id("t2") / tbody / tr / td[1]
outputs:
123
foo
bar
xyz
Since 1 means select all td elements which are the first child of their own direct parent.
But the following xpath:
(id("t2") / tbody / tr / td)[1]
outputs:
123

Resources