Passing parameters from script to XSL - xpath

Using XSLT2 with the latest Saxon HE.
I'm trying to pass multiple coordinate parameters from a script to XSL in order to filter results based on a location boundary box
Script:
java -jar saxon9he.jar -s:litter_bins.xml -o:"bins.xml" -xsl:"Split xml coords.xsl" Coord_2=51.3725 Coord_4=51.3751 Coord_1=-2.3615 Coord_3=-2.3572
XSL:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="Coord_2" select="Coord_2"/>
<xsl:param name="Coord_4" select="Coord_4"/>
<xsl:param name="Coord_1" select="Coord_1"/>
<xsl:param name="Coord_3" select="Coord_3"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="node[#lat[ . < $Coord_2 or . > $Coord_4 ] or #lon[ . < $Coord_1 or . > $Coord_3]]"/>
</xsl:stylesheet>
The above returns:
<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="JOSM"/>
However if I hard code the coordinates into the match xpath, it returns the expected results.
Xpath:
<xsl:template match="node[#lat[ . < 51.3725 or . > 51.3751 ] or #lon[ . < -2.3615 or . > -2.3572]]"/>
Results:
<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="JOSM">
<node id="-102973" visible="true" lat="51.37283499216" lon="-2.359890029">
<tag k="date_creat" v="17/07/2014 07:59:04 AM UTC"/>
<tag k="form_recor" v="888"/>
</node>
<snip...>
</osm>
What am I misunderstanding?

Try to declare a numeric type for the parameters e.g. <xsl:param name="Coord_2" as="xs:double"/> or <xsl:param name="Coord_2" as="xs:decimal"/>. Of course for that your stylesheet needs to declare xmlns:xs="http://www.w3.org/2001/XMLSchema" as a namespace declaration on the root element.
Without a numeric type I think the comparison will be of two xs:untypedAtomic values and then https://www.w3.org/TR/xpath-31/#id-general-comparisons demands
If both atomic values are instances of xs:untypedAtomic, then the
values are cast to the type xs:string
and then the string comparison of negative numbers fails to give you the wanted result.

Related

XSLT apply templates select condition on node list

I have an xml with a list and wanted to apply template on that which will send only specific nodes by a condition, but it is applying on the whole list. Could someone if I am missing anything, I am relatively new to XSL.
The condition I wanted to apply is if dep is 7 and no city tag exists, I started with condition to check if dep is 7. After apply template if i print my list, it is getting all of them, Instead of dep just with value 7.In my output I expect not to have dep with value 9.
Input XML:
<employeeList>
<employee>
<dep>7</dep>
<salary>900</salary>
</employee>
<employee>
<dep>7</dep>
<city>LA</city>
<salary>500</salary>
</employee>
<employee>
<dep>9</dep>
<salary>600</salary>
</employee>
<employee>
<dep>7</dep>
<salary>800</salary>
</employee>
</employeeList>
My XSL:
<xsl:apply-templates select="employeeList[employee/dep = '7']" mode="e"/>
<xsl:template match="employeeList" mode="e">
<xsl:for-each select="employee">
<dep>
<xsl:value-of select="dep" />
</dep>
</xsl:for-each>
Output XML:
<dep>7</dep><dep>7</dep><dep>9</dep><dep>7</dep>
The condition I wanted to apply is if dep is 7 and no city tag exists
Such condition can be easily implemented using e.g.:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/employeeList">
<root>
<xsl:for-each select="employee[dep='7' and not(city)]">
<dep>7</dep>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
Or shortly:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/employeeList">
<root>
<xsl:copy-of select="employee[dep='7' and not(city)]/dep"/>
</root>
</xsl:template>
</xsl:stylesheet>
But it's hard to see the point in outputting X number of <dep>7</dep> elements.
You select the employeeList based on a condition on its employee/dep, but once you have selected it, that condition no longer matters, and the <xsl:for-each select="employee"> selects all employees, regardless of their dep.
You can repeat the condition in the xsl:for-each statement:
<xsl:for-each select="employee[dep = '7']">

XSLT how to display/output duplicate values based on different element node and attributes

I am trying to output duplicate values across different nodes and value by using XSLT. I want the node element to be dynamic so it can track different value after the namespace prefix, for example: car:ID, car:Name, car:Location_name, or more. I know i can use the function Local-Name(.) but I am not sure how to apply to my XSLT logic. please help
the sample XML as follow:
<car:root xmlns:car="com.sample">
<Car_Input_Request>
<car:Car_Details>
<car:ID>Car_001</car:ID>
<car:Name>Fastmobile</car:Name>
<car:Local_Name>New York</car:Local_Name>
<car:Transmission_Reference_Type>
<car:ID car:type="Transmission_Reference_Type">Automatic</car:ID>
</car:Transmission_Reference_Type>
</car:Car_Details>
</Car_Input_Request>
<Car_Input_Request>
<car:Car_Details>
<car:ID>Car_002</car:ID>
<car:Name>Slowmobile</car:Name>
<car:Local_Name>New York</car:Local_Name>
<car:Transmission_Reference_Type>
<car:ID car:type="Transmission_Reference_Type">Manual</car:ID>
</car:Transmission_Reference_Type>
</car:Car_Details>
</Car_Input_Request>
<Car_Input_Request>
<car:Car_Details>
<car:ID>Car_001</car:ID>
<car:Name>Fastmobile</car:Name>
<car:Local_Name>New York</car:Local_Name>
<car:Transmission_Reference_Type>
<car:ID car:type="Transmission_Reference_Type">Automatic</car:ID>
</car:Transmission_Reference_Type>
</car:Car_Details>
</Car_Input_Request>
</car:root>
The XSLT used:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:car="com.sample"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="3.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:value-of select="//car:ID[ let $v:=string(.),$t:=#car:type return not( preceding::car:ID[string(.) = $v and #car:type=$t]) ]/(let $v:=string(.), $t:=#car:type,$c:=1+count(following::car:ID[string(.)=$v and $t=#car:type]) ,$c:=1+count(following::car:*[string(.)=$v]) return if ($c > 1) then concat( string(.), ' occurs ', $c, ' times for type ', $t, '
') else () )"/>
</xsl:template>
</xsl:stylesheet>
output shown from xslt:
Car_001 occurs 2 times for type
Automatic occurs 2 times for type Transmission_Reference_Type
But I want it to show
Car_001 occurs 2 times for type ID
Fastmobile occurs 2 times for type Name
Automatic occurs 2 times for type Transmission_Reference_Type
New York occurs 3 times for type Local_Name
If you are looking for an XSLT solution (rather than a single line XPath expression), you could make use of xsl:for-each-group with a composite key:
<xsl:stylesheet
xmlns:car="com.sample"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
expand-text="yes"
version="3.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:for-each-group select="//car:Car_Details/*" group-by="local-name(), normalize-space()" composite="yes">
<xsl:if test="current-group()[2]">
<xsl:text>{normalize-space()} occurs {count(current-group())} times for {local-name()}
</xsl:text>
</xsl:if>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>

xslt Merge children of 2 parents and Store in a variable

I receive an xml input like this:
<root>
<Tuple1>
<child11></child11>
<child12></child12>
<child13></child13>
</Tuple1>
<Tuple1>
<child11></child11>
<child12></child12>
</Tuple1>
<Tuple2>
<child21></child21>
<child22></child22>
</Tuple2>
<Tuple2>
<child21></child21>
<child22></child22>
<child23></child23>
</Tuple2>
</root>
How can I merge the children of each Tuple1 with children of Tuple2 and store them in a variable that will be used in the rest of xslt document?
First tuple1 will be merged with first Tuple2 and second Tuple1 will be merged with 2nd Tuple2 and so on. The merged output that should be stored in variable would look like this in memory:
<root>
<Tuple1>
<child11></child11>
<child12></child12>
<child13></child13>
<child21></child21>
<child22></child22>
</Tuple1>
<Tuple1>
<child11></child11>
<child12></child12>
<child21></child21>
<child22></child22>
<child23></child23>
</Tuple1>
</root>
Is variable the best option? If we use variable, is it created once or it is created every time called?
I use xslt 3.0 so solution for any version can help.
Thanks and I appreciate your help)
Here is a minimal XSLT 3 approach:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="root">
<xsl:variable name="temp1">
<xsl:copy>
<xsl:apply-templates select="Tuple1"/>
</xsl:copy>
</xsl:variable>
<xsl:copy-of select="$temp1"/>
</xsl:template>
<xsl:template match="Tuple1">
<xsl:copy>
<xsl:copy-of select="*, let $pos := position() return ../Tuple2[$pos]/*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Online at https://xsltfiddle.liberty-development.net/bdxtqg, I have used XPath's let instead of XSLT's xsl:variable to store the position to access the specific Tuple2.

Failing XPath on xsl:variable with XML fragment

I must be missing something very basic. I want to look up a key from a transformed XML document in XML fragment stored in xsl:variable. Here's a minimal example:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<code>A</code>
XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:variable name="mappings">
<mapping key="A">Amy</mapping>
</xsl:variable>
<xsl:template match="code">
<xsl:value-of select="$mappings/mapping[#key = text()]"/>
</xsl:template>
</xsl:stylesheet>
Transforming this XML document with the XSL stylesheet produces empty result. It seems that the comparison #key = text() is wrong, because when I use <xsl:value-of select="$mappings/mapping[#key = 'A']"/>, it retrieves the expected value (i.e. "Amy"). What am I missing?
Use
<xsl:template match="code">
<xsl:value-of select="$mappings/mapping[#key = current()]"/>
</xsl:template>
With an intermediate variable, it works properly :
<xsl:template match="code">
<xsl:variable name="keyval" select="./text()" />
<out>
<xsl:value-of select="$mappings/mapping[#key = $keyval]"/>
</out>
</xsl:template>

How to order self-referencing xml

I have a list of order lines with each one product on them. The products in may form a self-referencing hierarchy. I need to order the lines in such a way that all products that have no parent or whose parent is missing from the order are at the top, followed by their children. No child may be above its parent in the end result.
So how can i order the following xml:
<order>
<line><product code="3" parent="1"/></line>
<line><product code="2" parent="1"/></line>
<line><product code="6" parent="X"/></line>
<line><product code="1" /></line>
<line><product code="4" parent="2"/></line>
</order>
Into this:
<order>
<line><product code="6" parent="X"/></line>
<line><product code="1" /></line>
<line><product code="2" parent="1"/></line>
<line><product code="3" parent="1"/></line>
<line><product code="4" parent="2"/></line>
</order>
Note that the order within a specific level is not important, as long as the child node follows at some point after it's parent.
I have a solution which works for hierarchies that do not exceed a predefined depth:
<order>
<xsl:variable name="level-0"
select="/order/line[ not(product/#parent=../line/product/#code) ]"/>
<xsl:for-each select="$level-0">
<xsl:copy-of select="."/>
</xsl:for-each>
<xsl:variable name="level-1"
select="/order/line[ product/#parent=$level-0/product/#code ]"/>
<xsl:for-each select="$level-1">
<xsl:copy-of select="."/>
</xsl:for-each>
<xsl:variable name="level-2"
select="/order/line[ product/#parent=$level-1/product/#code ]"/>
<xsl:for-each select="$level-2">
<xsl:copy-of select="."/>
</xsl:for-each>
</order>
The above sample xslt will work for hierarchies with a maximum depth of 3 levels and is easily extended to more, but how can i generalize this and have the xslt sort arbitrary levels of depth correctly?
To start with, you could define a couple of keys to help you look up the line elements by either their code or parent attribute
<xsl:key name="products-by-parent" match="line" use="product/#parent" />
<xsl:key name="products-by-code" match="line" use="product/#code" />
You would start off by selecting the line elements with no parent, using a key to do this check:
<xsl:apply-templates select="line[not(key('products-by-code', product/#parent))]"/>
Then, within the template that matches the line element, you would just copy the element, and then select its "children" like so, using the other key
<xsl:apply-templates select="key('products-by-parent', product/#code)"/>
This would be a recursive call, so it would recursively look for its children until no more are found.
Try this XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="products-by-parent" match="line" use="product/#parent"/>
<xsl:key name="products-by-code" match="line" use="product/#code"/>
<xsl:template match="order">
<xsl:copy>
<xsl:apply-templates select="line[not(key('products-by-code', product/#parent))]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="line">
<xsl:call-template name="identity"/>
<xsl:apply-templates select="key('products-by-parent', product/#code)"/>
</xsl:template>
<xsl:template match="#*|node()" name="identity">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Do note the use of the XSLT identity transform to copy the existing nodes in the XML.
Very interesting problem. I would do this in two passes: first, nest the elements according to their hierarchy. Then output the elements, sorted by the count of their ancestors.
XSLT 1.0 (+ EXSLT node-set() function):
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="product-by-code" match="product" use="#code" />
<!-- first pass -->
<xsl:variable name="nested">
<xsl:apply-templates select="/order/line/product[not(key('product-by-code', #parent))]" mode="nest"/>
</xsl:variable>
<xsl:template match="product" mode="nest">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates select="../../line/product[#parent=current()/#code]" mode="nest"/>
</xsl:copy>
</xsl:template>
<!-- output -->
<xsl:template match="/order">
<xsl:copy>
<xsl:for-each select="exsl:node-set($nested)//product">
<xsl:sort select="count(ancestor::*)" data-type="number" order="ascending"/>
<line><product><xsl:copy-of select="#*"/></product></line>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your input, the result is:
<?xml version="1.0" encoding="UTF-8"?>
<order>
<line>
<product code="6" parent="X"/>
</line>
<line>
<product code="1"/>
</line>
<line>
<product code="3" parent="1"/>
</line>
<line>
<product code="2" parent="1"/>
</line>
<line>
<product code="4" parent="2"/>
</line>
</order>
This still leaves the issue of the existing/missing parent X - I will try to address that later.

Resources