Xpath that evaluates -all- attributes? - xpath

I maybe be trying to do something invalid here, but maybe someone smarter than me knows the correct syntax to solve my problem.
Given:
<group code="vehicle">
<line code="car">
<cell>
<box code="price">1000.00</box>
</cell>
</line>
<line code="car">
<cell code="sports">
<box code="price">500.00</box>
</cell>
</line>
</group>
If I use //*[#code="vehicle"]//*[#code="car"]//*[#code="price"], I will get both boxes returned (1000.00 and 500.00)--as expected but not what I want.
Is there an xpath syntax that will evaluate against all nodes that have an attribute of #code rather than skipping it if it doesn't match with the end result being that I only get back the first box (price of 1000.00)? Like asking, choose the first node with #code and that #code must equal "vehicle", then choose the next node with #code and that #code must equal "car", then choose the next node with #code and #code must equal "price".

Use:
"//box
[(#code='car' or #code='price' or #code='vehicle')
and
not(
ancestor-or-self::*
[
#code
and
not(#code='car'
or
#code='price'
or
#code='vehicle'
)
]
)
]

Not an expert on xpath but XmlSpy seems to indicate that this would work?
(//*[#code="vehicle"]//*[#code="car"]//*[#code="price"])[1]

Related

Xpath query expression: summing up a attribute over a condition

<Cities>
<city>
<name />
<country />
<population asof = "2019" />
<total> 2918695</total>
<Average_age> 28 </Average_age>
</city>
<city>
<name />
<country />
<population asof = "2020" />
<total> 78805467 </total>
<Average_age> 32 </Average_age>
</city>
</Cities>
I want to build a Xpath query which returns the total population of cities where asof is higher than 2018
Try this XPath-1.0 expression:
sum(/Cities/city[population/#asof > 2018]/total)
Or, another, less specific, version:
sum(//city[population/#asof > 2018]/total)
the expression to grab population with asof attribute greater than 2018 would be:
//population[#asof > '2018']
If you looking for <total> which is a sibling of <population> despite your indentation use following-sibling::total after the expression
otherwise use /total
lets follow the first approach so the XPath continues as:
//population[#asof > '2019']/following-sibling::total
and add /text() at the end to get text inside of desired <total> tag. additionally if you want sum of populations you can put the whole expression inside sum() function. the inside expression of sum gonna be like:
//population[#asof > '2019']/following-sibling::total/text()

iReport sub-report with Xpath expression

This should be very easy, but my sub report report stays empty.
my xml :
<?xml version="1.0" encoding="UTF-8"?>
<document>
<settings></settings>
<header></header>
<lines>
<line>
<product_level>1</product_level>
<product_description>description</product_description>
</line>
<line>
<product_level>2</product_level>
<product_description>description lvl2</product_description>
</line>
<line>
<product_level>3</product_level>
<product_description>description lvl3</product_description>
</line>
</lines>
</document>
my main document uses Xpath /document to select the data
the sub report uses Xpath /document/lines/line to select the data
the problem starts with the subreport where I only want to select certain values, say where
is 1 ( 1 )
using :
((net.sf.jasperreports.engine.data.JRXmlDataSource)$P{REPORT_DATA_SOURCE}).subDataSource("/document/lines/line/product_level[text()=3]")
results in a empty part.
If I leave out the /product_level[text()=3] it returns every line ( as expected )...
I am guessing it is something easy, but can't figure out what.
This is the correct XPath:
/document/lines/line/product_level[text()='3']
Here's a free tool to help in the future: http://www.bubasoft.net/product/xpath-builder/

Determine if any element with a given name has a particular value

Given this XML fragment (I've removed superfluous fluff):
<Event name="DataComplete">
<Task id="d20a0053-7678-43ba-bc8a-ece24dcff15b"/>
<DataItems>
<DataItem name="Survey" type="task">
<Value status="NotStarted" taskId="00000000-0000-0000-0000-000000000000" />
</DataItem>
<GroupDataItem name="CT_Visit"> --- this may repeat
<ItemGroup id="1" >
<DataItem name="Special Contractor" type="string">Yes</DataItem>
What xPath expression will determine if any DataItem with name="Special Contractor" has the value "Yes".
I'm trying something like this:
Yes = /Event/Task/DataItems/GroupDataItem/ItemGroup/DataItem/#[normalize-space() = 'Special Contractor']
and many variations usually resulting in "invalid xPath expression".
Any clues most welcome. Thanks!
[EDIT]
Thanks for the answers Jiri and Will. Will was close, but as my question states, I'm trying to determine if any* element has the value Yes. I should have been more explicit in saying that I need a boolean, true or false. Adapting Will's answer led me to this:
"Yes" = //Event/Task/DataItems/GroupDataItem/ItemGroup/DataItem[#name='Special Contractor']
This returns a simple Boolean='true' or Boolean='false'.
Thanks guys!
/Event/DataItems/GroupDataItem/ItemGroup/DataItem[#name = "Special Contractor"][. = "Yes"]
Returns the DataItem in question. Note that this will be a sequence of matching DataItem elements if there are more than one. If you just want a boolean:
exists(/Event/DataItems/GroupDataItem/ItemGroup/DataItem[#name = "Special Contractor"][. = "Yes"])
(as an aside; I removed Task from the xpath, since it's not actually an ancestor of the DataItem in the XML fragment you posted, even though the indentation makes it look like it is.)
Use this xpath
/Event/Task/DataItems/GroupDataItem/ItemGroup/DataItem[#name='Special Contractor']
for following xml:
<Event name="DataComplete">
<Task id="d20a0053-7678-43ba-bc8a-ece24dcff15b">
<DataItems>
<DataItem name="Survey" type="task">
<Value status="NotStarted" taskId="00000000-0000-0000-0000-000000000000" />
</DataItem>
<GroupDataItem name="CT_Visit"> --- this may repeat
<ItemGroup id="1" >
<DataItem name="Special Contractor" type="string">Yes</DataItem>
</ItemGroup>
</GroupDataItem>
</DataItems>
</Task>
...
</Event>
If the task is really non-pair element, then omit it from the xpath expression.

How do I force parsing an XML node as hash array?

This is my simplified myXML:
<?xml version="1.0" encoding="utf-8"?>
<ShipmentRequest>
<Message>
<MemberId>A00000001</MemberId>
<MemberName>Bruce</MemberName>
<Line>
<LineNumber>3.1</LineNumber>
<Item>fruit-004</Item>
<Description>Peach</Description>
</Line>
<Line>
<LineNumber>4.1</LineNumber>
<Item>fruit-001</Item>
<Description>Peach</Description>
</Line>
</Message>
</ShipmentRequest>
When I parse it with the Crack gem myHash is:
{
"MemberId"=>"A00000001",
"MemberName"=>"Bruce",
"Line"=>[
{"LineNumber"=>"3.1", "Item"=>"A0001", "Description"=>"Apple"},
{"LineNumber"=>"4.1", "Item"=>"A0002", "Description"=>"Peach"}
]
}
The Crack gem creates the hash Line as an array, because there two <Line> nodes in myXML. But if myXML would contain only one <Line> node, the Crack gem would not parse it as an array:
{
"MemberId"=>"ABC0001",
"MemberName"=>"Alan",
"Line"=> {"LineNumber"=>"4.1", "Item"=>"fruit-004", "Description"=>"Apple"}
}
I want to see it still as an array no matter if there's only one node:
{
"MemberId"=>"ABC0001",
"MemberName"=>"Alan",
"Line"=> [{"LineNumber"=>"4.1", "Item"=>"fruit-004", "Description"=>"Apple"}]
}
After you convert the XML document to a hash you could do this:
myHash["Line"] = [myHash["Line"]] if myHash["Line"].kind_of?(Hash)
It will ensure that the Line node will be wrapped in Array.
The problem is, you're relying on code to do what you really should do. Crack has no idea that you want a single node to be an array of a single element, and that behavior makes it a lot more difficult for you when trying to dive into that portion of the data.
Parsing XML isn't hard, and, by parsing it yourself, you'll know what to expect, and will avoid the hassle of dealing with the "sometimes it's an array and sometimes it's not" returned by Crack.
require 'nokogiri'
doc = Nokogiri::XML(<<EOT)
<?xml version="1.0" encoding="utf-8"?>
<ShipmentRequest>
<Message>
<MemberId>A00000001</MemberId>
<MemberName>Bruce</MemberName>
<Line>
<LineNumber>3.1</LineNumber>
<Item>fruit-004</Item>
<Description>Peach</Description>
</Line>
<Line>
<LineNumber>4.1</LineNumber>
<Item>fruit-001</Item>
<Description>Peach</Description>
</Line>
</Message>
</ShipmentRequest>
EOT
That sets up the DOM, so it can be navigated:
hash = {}
message = doc.at('Message')
hash[:member_id] = message.at('MemberId').text
hash[:member_name] = message.at('MemberName').text
lines = message.search('Line').map do |line|
line_number = line.at('LineNumber').text
item = line.at('Item').text
description = line.at('Description').text
{
:line_number => line_number,
:item => item,
:description => description
}
end
hash[:lines] = lines
message = doc.at('Message') finds the first <Message> node.
message.at('MemberId').text finds the first <MemberID> node inside <Message>.
message.at('MemberName').text is similar to the above step.
message.search('Line') looks for all <Line> nodes inside <Message>.
From those descriptions you can figure out the rest.
After running, hash looks like:
{:member_id=>"A00000001",
:member_name=>"Bruce",
:lines=>
[{:line_number=>"3.1", :item=>"fruit-004", :description=>"Peach"},
{:line_number=>"4.1", :item=>"fruit-001", :description=>"Peach"}]}
If I remove one of the <Line> blocks from the XML, and re-run, I get:
{:member_id=>"A00000001",
:member_name=>"Bruce",
:lines=>[{:line_number=>"3.1", :item=>"fruit-004", :description=>"Peach"}]}
Using search to locate the <Line> nodes is the trick. search returns a NodeSet, which is akin to an Array, so by iterating over it using map it'll return an array of hashes of the contents of <Line> tags.
Nokogiri is a great tool for parsing HTML and XML, then allowing us to search, add, change or remove nodes. It supports CSS and XPath accessors, so if you are used to jQuery or how CSS works, or XPath expressions, you'll be off and running quickly. The tutorials for Nokogiri are a good starting place to learn how it works.

Ireport IF Else Expression

I have this expression in iReport.:
($F{Q3_February}==0)?"-":$F{Q3_February}
Expression class is Double, I want this to returns as "-" if the value of $F{Q3_February} is 0.
I get "-" only if I change it to Expression class String but the problem is if the value is false it doesn't return a value of #,##0.00 %
Your problem is that the textField pattern is not applied if the expression class is not numeric. You are trying to conditionally change not only the value, but also the class. This is not possible with a single textField.
I think your best bet is to separate this into two textFields, one string and one double. Place them on top of each other and then use printWhenExpressions to hide the one that is not wanted. The end result will be similar to having a single field with a conditional expression, but gives you more flexibility with the other element properties (i.e. class and pattern).
Example:
<textField pattern="#,##0.00 %">
<reportElement x="200" y="80" width="100" height="20">
<printWhenExpression>
<![CDATA[$F{Q3_February}!=0]]>
</printWhenExpression>
</reportElement>
<textFieldExpression class="java.lang.Double">
<![CDATA[$F{Q3_February}]]>
</textFieldExpression>
</textField>
<textField>
<reportElement x="200" y="80" width="100" height="20">
<printWhenExpression>
<![CDATA[$F{Q3_February}==0]]>
</printWhenExpression>
</reportElement>
<textFieldExpression class="java.lang.String">
<![CDATA["-"]]>
</textFieldExpression>
</textField>
There are a few alternative ways [to GenericJon's suggestion to solve this]:
With 4.1.1 there is a field on text boxes called "Pattern Expression" it is a field based
field formatting option
You can format the field before it goes into the dataset [not recommended but works]
Lastly, you can call a scriptlet and pass in the values of the fields within the text section of the textbox.
[Define Scriptlet name="bob"]
<textFieldExpression class="java.lang.String">
<!CDATA[$P{[bob]_SCRIPTLET}.formatValues(fields1:n..)]]></textFieldExpression>

Resources