XPATH get default value when node is empty or not present - xpath

I have 3 types of data
<results>
<place>
<key>place</key>
<value>1</value>
</place>
</results>
OR
<results>
<place>
<key>place</key> // notice the missing value
</place>
</results>
OR
<results>
</results>
So my sample data will be like
<event>
<results>
<place>
<key>place</key>
<value>1</value>
</place>
<some additional data here>
</results>
</event>
<event>
<results>
<place>
<key>place</key>
</place>
<some additional data here>
</results>
</event>
<event>
<results>
<some additional data here>
</results>
</event>
I need an XPath expression that can give me a default value when <value> of <place> is present, null or missing. <place> can be missing as well in some cases as mentioned in my third sample data.
Output that I expect here is 1, <default-value>, <default-value>.
XPATH 2.0 solution will work as well. I have tried scourging stackoverflow and google but couldnt find anything.

Use:
//results/concat(place/value, for $r in . return 'default-value'[not($r/place/value)])
XSLT - based verification:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:sequence select=
"//results/concat(place/value, for $r in . return 'default-value'[not($r/place/value)])"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided (and completed) XML document:
<t>
<event>
<results>
<place>
<key>place</key>
<value>1</value>
</place>
<x/>
</results>
</event>
<event>
<results>
<place>
<key>place</key>
</place>
<y/>
</results>
</event>
<event>
<results>
<z/>
</results>
</event>
</t>
the XPath expression is evaluated and its results are copied to the output:
1 default-value default-value

I did it finally after a lot of trial and error.
{xpath::/events/event/(results//(place|rank)/value/string(), '')[1]}
the trick was to go one level up i.e. <results> in my case and then use the (if value present, default-value) XPATH notation.
Earlier, I was trying this unsuccessfully.
{xpath::/events/event/results//((place|rank)/value/string(), '')[1]}

Related

Find first occurence of node without traversing all of them using XPaths and elementpath library

I use elementpath to handle some XPath queries. I have an XML with linear structure which contains a unique id attribute.
<items>
<item id="1">...</item>
<item id="2">...</item>
<item id="3">...</item>
... 500k elements
<item id="500003">...</item>
</items>
I want the parser to find the first occurence without traversing all the nodes. For example, I want to select //items/item[#id = '3'] and stop after iterating over 3 nodes only (not over 500k of nodes). It would be a nice optimization for many cases.
An example using XSLT 3 streaming with a static parameter for the XPath, then using xsl:iterate with xsl:break to produce the "early exit" once the first item sought has been found would be
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all">
<xsl:param name="path" static="yes" as="xs:string" select="'items/item[#id = ''3'']'"/>
<xsl:output method="xml"/>
<xsl:mode on-no-match="shallow-copy" streamable="yes"/>
<xsl:template match="/" name="xsl:initial-template">
<xsl:iterate _select="{$path}">
<xsl:if test="position() = 1">
<xsl:copy-of select="."/>
<xsl:break/>
</xsl:if>
</xsl:iterate>
</xsl:template>
</xsl:stylesheet>
You can run it with SaxonC EE (unfortunately streaming is only supported by EE) and Python with e.g.
import saxonc
with saxonc.PySaxonProcessor(license=True) as proc:
print("Test SaxonC on Python")
print(proc.version)
xslt30proc = proc.new_xslt30_processor()
xslt30proc.set_parameter('path', proc.make_string_value('/items/item[#id = "2"]'))
transformer = xslt30proc.compile_stylesheet(stylesheet_file='iterate-items-early-exit1.xsl')
xdm_result = transformer.apply_templates_returning_value(source_file='items-sample1.xml')
if transformer.exception_occurred:
print(transformer.error_message)
print(xdm_result)

XML sorted list of only certain nodes

I have the following XML file. I need to print a list of only selected nodes (Total) in ascending order. I tried to use sort function, but there were some mistakes I couldn't identify and it returned everything, including values of other nodes in the initial file.
XML input:
<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<From>
<Name>Lucy</Name>
<Country>UK</Country>
</From>
<To>
<Name>John</Name>
<Country>US</Country>
</To>
<Items>
<Position>
<Name>Table</Name>
<Total>1</Total>
</Position>
<Position>
<Name>Chair</Nr>
<Total>4</Total>
</Position>
<Position>
<Name>Cup</Name>
<Total>5</Total>
</Position>
<Position>
<Name>Box</Name>
<Total>4</Total>
</Position>
</Items>
</Invoice>
How could I get the required output using?
Any help is greatly appreciated! Thank you!
One obvious approach to generate the desired output from the given input would be using an xsl:for-each also making use of xsl:sort:
<xsl:template match="/Invoice">
<SortedTotalList xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<xsl:for-each select="Positions/Position">
<xsl:sort select="Total"/>
<xsl:copy-of select="Total" />
</xsl:for-each>
</SortedTotalList>
</xsl:template>
Output is:
<?xml version="1.0"?>
<SortedTotalList xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Total>1</Total>
<Total>4</Total>
<Total>4</Total>
<Total>5</Total>
</SortedTotalList>

How do I use the msxsl:node-set to get a node set that I can use in a template parameter?

TL;DR; Why can't I use the element name in the XPATH going against a msxsl:node-set? It always returns nothing, as if the node-set is empty, when debugging shows that it is not empty.
Details: I need to use a node-set in an XSLT 1.0 document because my source XML is missing an important node. Instead of having to rewrite the entire XSLT, I'd like to instead inject a node-set so that my XSLT processing can continue as normal. I would like to use XPATH on the node-set but I am not able to use the actual element names, instead only a * works, but I am not sure why, or how I can access the actual element names in the XPATH.
Here is my XML (example only, the XML document here is the least important, see XSLT):
<?xml version="1.0" encoding="utf-8" ?>
<?xml-stylesheet type="text/xsl" href="generic.xslt" ?>
<ParentNode xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" i:noNamespaceSchemaLocation="generic.xsd">
<SomeChildNode>text</SomeChildNode>
</ParentNode>
Here is my XSLT:
<?xml version="1.0" encoding="utf-16"?>
<xsl:stylesheet version="1.0" xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:a="http://schemas.datacontract.org/2004/07/MeM.BizEntities" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<xsl:output method="xml" indent="yes" encoding="utf-16" omit-xml-declaration="no" />
<!-- Global Variables, used in multiple places -->
<xsl:variable name="empty"/>
<!-- Match Templates -->
<xsl:template match="ParentNode">
<ArrayOfSalesOrder>
<xsl:for-each select="SomeChildNode">
<xsl:call-template name="SomeChildNodeTemplate">
<xsl:with-param name="order" select="."/>
</xsl:call-template>
</xsl:for-each>
</ArrayOfSalesOrder>
</xsl:template>
<xsl:template name="SomeChildNodeTemplate">
<xsl:variable name="someRTF">
<Items>
<Item>
<Code>code</Code>
<Price>75</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<Code>code2</Code>
<Price>100</Price>
<Quantity>3</Quantity>
</Item>
</Items>
</xsl:variable>
<xsl:call-template name="ItemsTemplate">
<xsl:with-param name="items" select="msxsl:node-set($someRTF)"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="ItemsTemplate">
<xsl:param name="items"/>
<ItemsTransformed>
<xsl:for-each select="$items/Item">
<NewItem>
<NewCode>
<xsl:value-of select="Code"/>
</NewCode>
</NewItem>
</xsl:for-each>
</ItemsTransformed>
<ItemsTransformedThatWorksButNotHowIWant>
<xsl:for-each select="$items/*/*">
<NewItem>
<NewCode>
<xsl:value-of select="*[1]"/>
</NewCode>
<NewPrice>
<xsl:value-of select="*[2]"/>
</NewPrice>
<NewQuantity>
<xsl:value-of select="*[3]"/>
</NewQuantity>
</NewItem>
</xsl:for-each>
</ItemsTransformedThatWorksButNotHowIWant>
</xsl:template>
</xsl:stylesheet>
I would expect to be able to use XPATH to query into the node-set such that I can use their proper element names. This doesn't seem to be the case, and I'm struggling to understand why. I know there can be namespacing issues, but trying *:Item etc. doesn't work for me. I am able to use *[local-name()='Item'] but this seems like a horrible work around, not to mention that I'll have to rewrite any downstream templates and that is what I'm trying to avoid by using the node-set in the first place.
Result:
<?xml version="1.0" encoding="utf-16"?>
<ArrayOfSalesOrder xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:a="http://schemas.datacontract.org/2004/07/MeM.BizEntities" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ItemsTransformed />
<ItemsTransformedThatWorksButNotHowIWant>
<NewItem>
<NewCode>code</NewCode>
<NewPrice>75</NewPrice>
<NewQuantity>1</NewQuantity>
</NewItem>
<NewItem>
<NewCode>code2</NewCode>
<NewPrice>100</NewPrice>
<NewQuantity>3</NewQuantity>
</NewItem>
</ItemsTransformedThatWorksButNotHowIWant>
</ArrayOfSalesOrder>
As you can see, I can get it to work with * but this is not very usable on a more complex structure. What am I doing wrong? Does this have to do with namespaces?
I would expect to see something under the <ItemsTransformed /> node, but instead it is just empty, and so far I can't get anything except the * to work.
The SO question below is what I was using, I thought I had an answer there, but I can't get the XPATH to work.
Reference:
XSLT 1.0 - Create node set and pass as a parameter
The problem here is that your stylesheet has a default namespace:
xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2"
Therefore, when you do:
<xsl:variable name="someRTF">
<Items>
<Item>
<Code>code</Code>
<Price>75</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<Code>code2</Code>
<Price>100</Price>
<Quantity>3</Quantity>
</Item>
</Items>
</xsl:variable>
you are populating your variable with elements in the default namespace, so the variable actually contains:
<Items xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2">
<Item>
<Code>code</Code>
<Price>75</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<Code>code2</Code>
<Price>100</Price>
<Quantity>3</Quantity>
</Item>
</Items>
Naturally, when you try later to select something like:
<xsl:for-each select="xyz:node-set($someRTF)/Items/Item">
you select nothing, because both Items and Item are in the default namespace and you're not calling them by their fully qualified name.
--- edit: ---
The problem can be easily solved by making sure that the root element of the variable - and by extension, all its descendants - are in no namespace.
Here's a simplified example (will run with any input):
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:variable name="someRTF">
<Items xmlns="">
<Item>
<Code>code</Code>
<Price>75</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<Code>code2</Code>
<Price>100</Price>
<Quantity>3</Quantity>
</Item>
</Items>
</xsl:variable>
<xsl:template match="/">
<ArrayOfSalesOrder>
<ItemsTransformed>
<xsl:for-each select="exsl:node-set($someRTF)/Items/Item">
<NewItem>
<NewCode>
<xsl:value-of select="Code"/>
</NewCode>
</NewItem>
</xsl:for-each>
</ItemsTransformed>
</ArrayOfSalesOrder>
</xsl:template>
</xsl:stylesheet>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<ArrayOfSalesOrder xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2">
<ItemsTransformed>
<NewItem>
<NewCode>code</NewCode>
</NewItem>
<NewItem>
<NewCode>code2</NewCode>
</NewItem>
</ItemsTransformed>
</ArrayOfSalesOrder>

xquery filter on attribute and element

I have the following simple XML document:
<?xml version="1.0" encoding="UTF-8"?>
<cars>
<car>
<data attrib="Make">
<text>Volvo</text>
</data>
<data attrib="Model">
<text>855</text>
</data>
</car>
<car>
<data attrib="Make">
<text>Volvo</text>
</data>
<data attrib="Model">
<text>745</text>
</data>
</car>
<car>
<data attrib="Make">
<text>Volvo</text>
</data>
<data attrib="Model">
<text>V70R</text>
</data>
</car>
</cars>
And the following XPath:
/cars/car/data[(#attrib='Model') and (text='855')]
This returns the following result:
<data attrib="Model"><text>855</text></data>
I want the XPath to return the whole <car> block for the match.
So return data would be like this:
<cars>
<car>
<data attrib="Make">
<text>Volvo</text>
</data>
<data attrib="Model">
<text>855</text>
</data>
</car>
</cars>
How would I modify the XPath expression above to achieve this?
XPath returns whatever node you go up to - in your case you're going to data, so that's what you're getting back. If you want car instead, place your predicate after car.
/cars/car[data/#attrib='Model' and data/text='855']
Or, slightly shorter
/cars/car[data[#attrib='Model' and text='855']]
XQuery to produce the desired output:
<cars>
{/cars/car[data[#attrib='Model' and text='855']]}
</cars>
Here is a complete and likely one of the shortest possible XSLT solutions:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" />
<xsl:template match="/*">
<cars>
<xsl:copy-of select="car[data[#attrib='Model' and text='855']]"/>
</cars>
</xsl:template>
</xsl:stylesheet>
However, the following transformation, using the wellknown identity rule is both easier to write and provides maximum flexibility, extensibility and maintainability:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="car[not(data[#attrib='Model' and text='855'])]"/>
</xsl:stylesheet>
When either of these two transformations is applied on the provided XML document:
<cars>
<car>
<data attrib="Make">
<text>Volvo</text>
</data>
<data attrib="Model">
<text>855</text>
</data>
</car>
<car>
<data attrib="Make">
<text>Volvo</text>
</data>
<data attrib="Model">
<text>745</text>
</data>
</car>
<car>
<data attrib="Make">
<text>Volvo</text>
</data>
<data attrib="Model">
<text>V70R</text>
</data>
</car>
</cars>
the wanted, correct result is produced:
<cars>
<car>
<data attrib="Make">
<text>Volvo</text>
</data>
<data attrib="Model">
<text>855</text>
</data>
</car>
</cars>
Explanation:
The first transformation generates the top element cars, then simply selects the wanted car element and copies it as the body of cars.
The second transformation is based on one of the most fundamental and powerful XSLT design patterns -- using and overriding the identity rule.
The identity template copies every matched node (for which it is selected to process) "as-is".
There is one template overriding the identity rule. This template matches any car for which it is not true that data[#attrib='Model' and text='855']. The body of the template is empty and this results in nothing from the matched car element being copied to the output -- in other words we can say that amy matching car element is "deleted".

Grand sum of distinct values in xslt1.0

<customer>
<item>
<BILLNO>1</BILLNO>
<product>ABC</product>
<AMT>20</AMT>
</item>
<item>
<BILLNO>2</BILLNO>
<product>GHK</product>
<AMT>30</AMT>
</item>
<item>
<BILLNO>1</BILLNO>
<product>XYZ</product>
<AMT>20</AMT>
</item>
</customer>
I am trying to take grand sum of distinct values using xslt1.0.
I want the output like this using muenchian method.each bill will have multiple products. at the end of the day i need total number of bills and total amount
<sales>
<totalbills>2</totalbills>
<totalamount>50</totalamount>
</sales>
Thanks for help
ram
This Xslt stylesheet:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="item-key" match="item" use="BILLNO/text()"/>
<xsl:template match="/customer">
<root>
<xsl:for-each select="item[generate-id() = generate-id(key('item-key', BILLNO/text()))]">
<sales>
<totalbills>
<xsl:value-of select="count(../item[BILLNO = current()/BILLNO])"/>
</totalbills>
<totalamount>
<xsl:value-of select="sum(../item[BILLNO = current()/BILLNO]/AMT)"/>
</totalamount>
</sales>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
renders the following ouptut:
<?xml version="1.0" encoding="utf-8"?>
<root>
<sales>
<totalbills>2</totalbills>
<totalamount>40</totalamount>
</sales>
<sales>
<totalbills>1</totalbills>
<totalamount>30</totalamount>
</sales>
</root>
This short and simpler transformation (no xsl:for-each, no .., no text() useage):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kBills" match="item" use="BILLNO"/>
<xsl:variable name="vdistItems" select=
"/*/*[generate-id() = generate-id(key('kBills', BILLNO)[1])]"/>
<xsl:template match="/*">
<sales>
<totalbills><xsl:value-of select="count($vdistItems)"/></totalbills>
<totalamount><xsl:value-of select="sum($vdistItems/AMT)"/></totalamount>
</sales>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<customer>
<item>
<BILLNO>1</BILLNO>
<product>ABC</product>
<AMT>20</AMT>
</item>
<item>
<BILLNO>2</BILLNO>
<product>GHK</product>
<AMT>30</AMT>
</item>
<item>
<BILLNO>1</BILLNO>
<product>XYZ</product>
<AMT>20</AMT>
</item>
</customer>
produces the exact wanted, correct result:
<sales>
<totalbills>2</totalbills>
<totalamount>50</totalamount>
</sales>
Explanation: Appropriate use of
The Muenchian method for grouping.
The sum() function.

Resources