Deepest match first with XPath - xpath

I have a configuration file in xml, with increasing specificity. That is, the closest setting to some section is the setting that will be in effect, even if it is set on higher levels. A colleague suggested using XPath for this. But I'm not sure how it would work? I suppose I want to find the "deepest match" for the setting, then automatically backing out if there is no match.
For instance:
<Settings>
<Foo>Bar</Foo>
<Products>
<Bar>Baz</Bar>
<Product Name="Thingamabob">
<Foo>Baz</Foo>
</Product>
<Product Name="Whizbang">
<Bar>Foo</Bar>
</Product>
</Products>
</Settings>
Searching for the Bar setting for Thingamabob should return "Baz", but "Foo" on Whizbang. In the same way, Foo has a value of "Baz" for Thingamabob, but "Bar" on Whizbang.
Can this type of lookup be implemented with XPath?

Searching Foo on Thingamabob can be done like so
//*[#Name='Thingamabob']/ancestor-or-self::*/Foo
But this will give you two nodes, you want to obtain the node with the maximum count(ancestor::*) and then get its text() value
I don't know of a way to do max on a xml hierarchy like yours, use an external script to iterate over the node list given by the xpath to find the max depth, then grab its content.
EDIT:
You may be able to do this in XSLT 2.0, note I have not tested this.
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:function name="my:lookup">
<xsl:param name="name"/>
<xsl:param name="arg"/>
<xsl:for-each select="//*[#Name='$name']/ancestor-or-self::*/$arg">
<xsl:sort select="count(ancestor::*)" data-type="number" order="descending"/>
<xsl:if test="position()=1"><xsl:value-of select="."/></xsl:if>
</xsl:for-each>
</xsl:function>
<xsl:template match="/">
<xsl:copy-of select="my:lookup('Thingamabob','Foo')" />
</xsl:template>
</xsl:stylesheet>
EDIT 2:
I've also figure this should work in XPATH 2.0 but I've not tested it
//*[#Name='Thingamabob']/ancestor-or-self::*/Foo[count(ancestor::*) >= //*[#Name='Thingamabob']/ancestor-or-self::*/Foo/count(ancestor::*)]/text()

If you know for certain that <Bar> will precede <Product>, you could do it with this:
//Product[#Name='Thingamabob']/preceding-sibling::Bar|//Product[#Name='Thingamabob']/Bar

Related

Nokogiri XSLT can transform with invalid stylesheet

If I try to transform sample xml (target.xml)
<?xml version="1.0"?>
<root />
with invalid XSL stylesheet (invalid.xsl):
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/1999/xhtml" version="1.0">
<xsl:template match="/root">
<new-root />
</xsl:stylesheet>
</xsl:template>
using Nokogiri (1.6.8.1) ruby v2.1.9:
require 'nokogiri'
print Nokogiri::XSLT(File.open('invalid.xsl'))
.transform(Nokogiri::XML(File.open('target.xml')))
it will finish successfully:
# ruby program.rb
<?xml version="1.0"?>
<new-root xmlns="http://www.w3.org/1999/xhtml"/>
How to configure nokogiri to validate XSL and throw an error if it's invalid?

How to read a parameter passed by maven on my stylesheet XSL?

I'm running this command:
mvn org.codehaus.mojo:xml-maven-plugin:transform "-DAPP=testingapp"
And inside my XSL I'm transforming a graphml to a HTML and I want to display this app name on the top of my HTML.
How do I read this attribute that I'm passing on the command line on my xsl?
Thank you!
Yes. It is possible.
In your pom.xml
<configuration>
<transformationSets>
<transformationSet>
<parameters>
<parameter>
<name>APP</name>
<value>${APP}</value>
</parameter>
</parameters>
</transformationSet>
</transformationSets>
</configuration>
In your xsl file
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:param name="APP" />
<xsl:value-of select="$APP"/>
</xsl:stylesheet>
You will need to declare in pom.xml and repeat in your xsl file. This is the trick.
Note: This also work for xslt-2.0

xslt debugging keeps debugging previous version of xslt document VS 2012

I'm trying to debug an xslt file in VS2012. When I hit XML -> Xslt Debugging at the top it keeps on showing the output from a previous version I debugged. I've many many changes to it since then and it's not reflecting that.
Here is a screenout :
http://imgur.com/b4Og6Nm
The code in my xslt file (which i'm pointing the input to an XML file)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<head>
</head>
<body>
<xsl:apply-templates select ="CommercialClients">
</xsl:apply-templates>
</body>
</html>
</xsl:template>
<xsl:template match="CommercialClients">
I found some data
</xsl:template>
</xsl:stylesheet>

How do I use XPath to count the number of nodes with a certain attribute

I can't seem to get an XPath expression to work for my scenario. I want to find all the "Device" nodes that have the type "EndDevice". I'm able to count all the "Device" nodes, and I'm also able to find all "Device" nodes with the "EndDevice" attribute. However, I can't seem to combine them!
count(//Device) //works
//Device[#xsi:type='EndDevice'] //works
count(//Device[#xsi:type='EndDevice']) //doesn't work
If it matters, I'm using XPathBuilder.
I reproduced it using XPathBuilder 2.0.0.4.
However the XPath expression works and evaluates correctly in an online evaluator I tried (http://www.whitebeam.org/library/guide/TechNotes/xpathtestbed.rhtm).
EDIT: Also tried with latest version of Altova XMLspy
input:
<?xml version="1.0"?>
<asdf xmlns:xsi="n/a">
<Device xsi:type='EndDevice'/>
<Device xsi:type='EndDevice'/>
<Device xsi:type='EndDevice'/>
<Device xsi:type='EndDevice'/>
</asdf>
xslt:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xsi="n/a">
<xsl:output indent="yes"/>
<xsl:template match="*">
<output>
<xsl:value-of select="count(//Device[#xsi:type = 'EndDevice'])"/>
</output>
</xsl:template>
</xsl:stylesheet>
output:
<?xml version="1.0" encoding="UTF-8"?>
<output xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xsi="n/a">4</output>
I think it's XPathBuilder thats doing something wrong.
Using the above xml saved into a test.xml and using the tool http://kernowforsaxon.sourceforge.net/
declare namespace xsi="n/a";
count(doc('test.xml')//Device[#xsi:type = "EndDevice"])
Produces the right output.

Format Date of XML Attribute Value Using XSLT

I'm running into an issue with an XSLT Transformation I'm trying to do. In the overall project, I'm working to convert an XML document to a CSV file for a database upload. The problem is the XML sends dates as an attribute value in ISO8601 format (YYYY-MM-DDT00:00:00), but for the database to take it from the CSV file, I need to convert it to MM/DD/YYYY.
Here's an XML Sample:
<GetChanged>
<UserInformation>
<Column Name="id" Value="555555555"/>
<Column Name="name" Value="Kevin"/>
<Column Name="bday" Value="1990-01-01T00:00:00"/>
</UserInformation>
</GetChanged>
Here's the XSLT I'm trying to use:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:csv="csv:csv">
<xsl:output method="text"/>
<xsl:template match="GetChanged">
<!-- header -->
<xsl:text>"id","name","bday"
</xsl:text>
<!-- data rows -->
<xsl:for-each select="UserInformation">
<!-- data cells -->
<xsl:for-each select="Column[#Name='id']">
<xsl:text>"</xsl:text>
<xsl:value-of select="#Value"/>
<xsl:text>"</xsl:text>
</xsl:for-each>
<xsl:text>,</xsl:text>
<xsl:for-each select="Column[#Name='name']">
<xsl:text>"</xsl:text>
<xsl:value-of select="#Value"/>
<xsl:text>"</xsl:text>
</xsl:for-each>
<xsl:text>,</xsl:text>
<xsl:for-each select="Column[#Name='bday']">
<xsl:text>"</xsl:text>
<xsl:value-of select="format-date(#Value, '[M01]/[D01]/[Y0001]')"/>
<xsl:text>"</xsl:text>
</xsl:for-each>
<xsl:if test="position()!=last()">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, when I run this through Ruby using Nokogiri, I end up getting an error:
RuntimeError: runtime error: element value-of
XPath evaluation returned no result
Thoughts on what I can do to format this date appropriately?
Thanks in advance!
nokogiri depends on libxslt for XSLT.
In Nokogiri 1.6.0 and later libxml2 and libxslt are bundled with the
gem, but if you want to use the system versions:
(see here for more details: https://github.com/sparklemotion/nokogiri)
libxslt only supports XSLT-1.0
A separate library called libxslt is available implementing XSLT-1.0
for libxml2
(Details see here: http://xmlsoft.org/XSLT.html)
You could work around that by using a solution like this: Convert date from DD-MMM-YYYY to YYYYMMDD format in xslt 1.0
or do some substring/concat like described here: Displaying Date as DD-MM-YYYY within XSLT/XML
A value expressed as YYYY-MM-DDT00:00:00is not a date. Try changing :
<xsl:value-of select="format-date(#Value, '[M01]/[D01]/[Y0001]')"/>
to:
<xsl:value-of select="format-dateTime(#Value, '[M01]/[D01]/[Y0001]')"/>
Note that both functions require an XSLT 2.0 processor. If your processor supports only XSLT 1.0, you can reformat the date using string manipulations:
<xsl:value-of select="substring(#Value, 6, 2)"/>
<xsl:text>/</xsl:text>
<xsl:value-of select="substring(#Value, 9, 2)"/>
<xsl:text>/</xsl:text>
<xsl:value-of select="substring(#Value, 1, 4)"/>

Resources