XSLT 3.0 xsl:mode on-no-match="shallow-skip" - xslt

I'd like to filter XML elements out of a response with XSLT 3 with Saxon HE 10.6
<!-- https://mvnrepository.com/artifact/net.sf.saxon/Saxon-HE -->
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>10.6</version>
</dependency>
I saved cases on https://xsltfiddle.liberty-development.net/3MP42Pc and https://xsltfiddle.liberty-development.net/3MP42Pc/1
I was hoping to be able to use
<xsl:mode on-no-match="shallow-skip" />
(that is: skip elements the do not match a filter.)
So I want to copy all elements that match some deeper attribute value
The structure is like a dataset of shoes with a status, appearing like generic items on a bill.
bill.xml
<bill>
<item>
<shoes>
<status>0</status>
</shoes>
</item>
<item>
<shoes>
<status>1</status>
</shoes>
</item>
<item>
<shoes>
<status>2</status>
</shoes>
</item>
</bill>
I want the generic items (of any kind (shoes)) with status=0
(otherwise said: skip items where '*/[status=0'] does not match)
bill.xslt
<xsl:stylesheet version="3.0">
<xsl:mode on-no-match="shallow-skip" />
<xsl:template match="item/*[status=0]"/>
</xsl:stylesheet>
The result would have to be
<bill>
<item>
<shoes><status>0</status></shoes>
</item>
</bill>
Alas, this script finds nothing
But. In the case of
<xsl:mode on-no-match="shallow-copy" />
It finds (as expected) all items that are not status=0
<bill>
<item/>
<item>
<shoes><status>1</status></shoes>
</item>
<item>
<shoes><status>2</status></shoes>
</item>
</bill>
If i use
<xsl:mode on-no-match="deep-copy" />
It finds all of the items (no filtering).
It seems not very logical to me, even though an item element has the context.
I use SAXON HE version 10.6, the only difference in the code with javax.xml.transform is the use of
TransformerFactory factory = new **BasicTransformerFactory**();
Question is how to make an elegant small script that does this: output the whole xml, skipping the items that do not match.

I think you want to use <xsl:mode on-no-match="shallow-copy"/> with <xsl:template match="item[not(*/status = 0)]"/> (or <xsl:template match="item/*[not(status = 0)]"/> if the item elements all need to be copied, only their children need to be filtered)).
I am not sure where you got the idea that <xsl:mode on-no-match="shallow-skip" /> should do unless you explicitly added <xsl:template match="item[*/status = 0]"><xsl:copy-of select="."/></xsl:template>. Even then you would lose the root element.
If you look at https://www.w3.org/TR/xslt-30/#built-in-templates-shallow-skip it says:
The effect of processing a tree using a mode that specifies
on-no-match="shallow-skip" is to drop both the textual content and the
markup from the result document, except where there is an explicit
user-written template rule that dictates otherwise.

To get the result you (apparently) expect, while using:
<xsl:mode on-no-match="shallow-skip" />
as the default, you would have to make your template:
<xsl:template match="*[descendant-or-self::status=0] | status[.=0]/text()">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
in order to copy all branches leading to a <status>0</status> leaf in their entirety.
As already noted, it is much simpler to make:
<xsl:mode on-no-match="shallow-copy" />
the default. Then you only need to add:
<xsl:template match="*[not(descendant-or-self::status=0)]"/>
in order to suppress the branches not having a <status>0</status> leaf.

Thanks Martin
So the logic goes a bit further.
the tip to explicitly write the node misses out of the root tag
https://xsltfiddle.liberty-development.net/3MP42Pc/2
so the solution is to use
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="item/*[not(status = 0)]"/>
see
https://xsltfiddle.liberty-development.net/3MP42Pc/3
Michael's suggestion with on-no-match="shallow-skip" also works including the root tag, diving into a descendant.
<xsl:template match="*[descendant-or-self::status=0] | status[.=0]/text()">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
https://xsltfiddle.liberty-development.net/3MP42Pc/4
The use of descendant-or-self is new to me though

Related

How do I match on any node that itself or any child has an attribute with a value in XSLT Template?

Say I have XML data like this:
<root>
<subs>
<sub>
<values>
<value attribute="a">1</value>
<value attribute="a">2</value>
<value attribute="c">3</value>
<value attribute="c">4</value>
</values>
</sub>
<subOther>
<otherValues attribute="c">
<otherValue attribute="a">1</value>
<otherValue attribute="a">2</value>
<otherValue attribute="b">3</value>
<otherValue attribute="a">4</value>
</otherValues>
</subOther>
</subs>
</root>
I am trying to create an XSLT template that matches all the nodes in the path to /root/subs/subOther/otherValues/otherValue[attribute="b"].
So far, this is the closest I have gotten:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:strip-space elements="*" />
<!--IDENTITY TEMPLATE -->
<xsl:template match="#*|node()">
<xsl:apply-templates select="node()" />
</xsl:template>
<xsl:template match="//*[ancestor-or-self::[#attribute='b']]">
<xsl:copy>
<xsl:apply-templates select="node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
But that throws an error saying there is an unexpected token [. I have tried several combinations but they either don't match anything at all, match too much (i.e. everything), or they throw some sort of error.
Edit: I updated the example and expected to be a little more clear. Also note that this is a highly-simplified XML. In my actual file the attribute in question can be at any leaf node on any valid element for that level, so I have to use a more generic path using * and unknown paths with //. So, for instance, one of the value elements could be the one with attribute="b" and it would trigger the same result.
Edit 2: The expected result is to select the nodes that have a path that lead to any left-child w/ an attribute that is equal to a specific value. In my XSD schema there's a total of about 100 possible leaf nodes spread all over the place. The use case is that the attribute in question marks which data elements have had changes, and I need to basically create a "diff" where the full file is whittled down to only nodes where the results are only those items that have changed and their parents. In the small example above, attrubute="b" is the indication I need to copy that node, and thus I would expect this exact result:
<root> <!-- Copied because part of the path -->
<subs> <!-- Copied because part of the path -->
<sub> <!-- Copied because part of the path -->
<values> <!-- Copied because part of the path -->
<value attribute="b">3</value> <!-- Copied because it matches the attribute -->
</values>
</sub>
</subs>
</root>
I hope that makes better sense. Also, I fixed the typo on the xsl:stylesheet being self-closing.
It looks like you have changed the identity template to ignore elements (the change will also drop attributes and text nodes), and added a template to copy the elements you need.
I think you need to reverse your logic. Instead of thinking about things you want to copy, think of it as removing things you don't want to copy.
So, you have the identity template to do the generic copying of elements, and have a second template to remove the things you don't want (the elements which don't have a "b" attribute either on its self or its descendants).
Try this XSLT
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:strip-space elements="*" />
<!--IDENTITY TEMPLATE -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="*[not(descendant-or-self::*[#attribute = 'b'])]" />
</xsl:stylesheet>
See it in action at http://xsltfiddle.liberty-development.net/ncntCS6

XSLT - Getting data from a similarly named sibling using data from the first

I am an XSLT newb whose learning by doing. I've managed to do some easier choose when, for each loops but don't understand what is required to do my next aim.
Here is a sample of the input xml.
What I am looking to do is where the of is Board, look at the value of (in the example below its B). Since B means bottom I then want to look at the sibling component where the value of is EdgeBottom, and return the value of from this sibling component.
Note that could be TBLR or any combination of those options, and I want to pull the Material details from each corresponding Component.
I will be outputting into a table but once I get the idea of how it can be done I can muck about this with. Please excuse any terminology errors above, and the lack of any non-working code examples. Many thanks.
<Report schema="1.0">
<Item id="74" name="cabinet">
<VSection id="0" vsection="main">
<HSection id="3">
<Component id="2" idfull="07400302">
<DisplayName>EdgeBottom</DisplayName>
<Category>Edging</Category>
<Brand>Edging</Brand>
<Color>Edging</Color>
<Material>0.4mm Edging</Material>
</Component>
<Component id="1" idfull="07400301">
<DisplayName>Board</DisplayName>
<Category>Carcass</Category>
<Brand>Laminate</Brand>
<Color>White</Color>
<Material>16White</Material>
<Edging>B</Edging>
<IsEdgedOnTop>No</IsEdgedOnTop>
<IsEdgedOnRight>No</IsEdgedOnRight>
<IsEdgedOnBottom>Yes</IsEdgedOnBottom>
<IsEdgedOnLeft>No</IsEdgedOnLeft>
<EdgeMatTop>0.4mm</EdgeMatTop>
<EdgeMatRight>0.4mm</EdgeMatRight>
<EdgeMatBottom>0.4mm</EdgeMatBottom>
<EdgeMatLeft>0.4mm</EdgeMatLeft>
</Component>
</HSection>
</VSection>
</Item>
<DocumentProperties>
</DocumentProperties>
</Report>
Assuming you were positioned on a 'Board' component....
<xsl:template match="Component[DisplayName='Board']">
Then you could use the preceding-sibling axis to get the Component element using a series of xsl:if conditions, like so:
<xsl:if test="contains(Edging, 'B')">
<xsl:apply-templates select="preceding-sibling::Component[DisplayName='EdgeBottom']"/>
</xsl:if>
<xsl:if test="contains(Edging, 'T')">
<xsl:apply-templates select="preceding-sibling::Component[DisplayName='EdgeTop']"/>
</xsl:if>
Then you could just have a template where you output the value for the 'Edging' component. For example
<xsl:template match="Component[Category='Edging']">
<edging>
<xsl:value-of select="Material" />
</edging>
</xsl:template>
If you wanted to simplify things, the xsl:if conditions could be combined into one single statement. If the possible values of the edging are indeed "EdgeTop", "EdgeBottom", "EdgeLeft" and "EdgeRight" then the fifth character of each of these are obviously "T", "B", "L" and "R" and so can be checked directly against the current component, like so...
<xsl:apply-templates
select="preceding-sibling::Component
[contains(current()/Edging, substring(DisplayName, 5, 1))]"/>
Here is a sample XSLT which demonstrates this in action...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:apply-templates select="//Component[DisplayName='Board']" />
</xsl:template>
<xsl:template match="Component[DisplayName='Board']">
<xsl:apply-templates select="preceding-sibling::Component[contains(current()/Edging, substring(DisplayName, 5, 1))]"/>
</xsl:template>
<xsl:template match="Component[Category='Edging']">
<edging>
<xsl:value-of select="Material" />
</edging>
</xsl:template>
</xsl:stylesheet>
Note that if the Component element could be a following sibling, not just a preceding one, then try this expression instead:
<xsl:apply-templates
select="../Component
[Category='Edging']
[contains(current()/Edging, substring(DisplayName, 5, 1))]"/>

HTML Escape some xml tag using xslt

I have some XML like
<ITEM><TITLE>Video: High Stakes for <KW>Obama</KW> & Romney</TITLE></ITEM>
which I would like to translate to the below XML using XSLT.
<ITEM><TITLE>Video: High Stakes for <KW>Obama</KW> & Romney</TITLE></ITEM>
I tried <xls:copy-of select="./TITLE"/> but that is not escaping the XML tags. Any idea?
These sorts of questions always attract a lot of competing answers, because the best answer depends on how generalized you need it to be, and that is not clear from your question.
If you want a super-generalised solution, I will leave that to the regular XSLT SO stars. Or you could just search SO for questions which ask how to serialize/ or stringize XML. There are plenty.
I will offer you a very simple but specific solution, for a very narrow interpretation of your question. For this I will assume that:
You are just interested in serializing the content of the TITLE element.
The element children of TITLE' (such asKW`) are restricted, in that they have no children and no attributes, and are not in a namespace.
With this interpretation, this input document...
<ITEM>
<TITLE>Video: High Stakes for <KW>Obama</KW> & Romney</TITLE>
</ITEM>
...transformed by this XSLT 1.0 style-sheet (works just the same in 2.0)...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:strip-space elements="*" />
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="TITLE/*">
<xsl:value-of select="concat('<',name(),'>',.,'</',name(),'>')" />
</xsl:template>
</xsl:stylesheet>
...yields...
<ITEM>
<TITLE>Video: High Stakes for <KW>Obama</KW> & Romney</TITLE>
</ITEM>
XSLT doesn't have a specific, direct feature to destruct markup to text (and you have to think well if this is really useful in your case).
This said, here is a short solution using a tiny C# extension function (a similar extension function can easily be written for most other PLs and platforms):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:my="my:my" exclude-result-prefixes="msxsl my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/*/TITLE/*">
<xsl:value-of select="my:stringize(.)"/>
<xsl:apply-templates/>
</xsl:template>
<msxsl:script language="c#" implements-prefix="my">
public string stringize(XPathNavigator doc)
{
return doc.OuterXml;
}
</msxsl:script>
</xsl:stylesheet>
When this transformation is applied to the provided XML document:
<ITEM><TITLE>Video: High Stakes for <KW>Obama</KW> & Romney</TITLE></ITEM>
the wanted, correct result is produced:
<ITEM>
<TITLE>Video: High Stakes for <KW>Obama</KW> & Romney</TITLE>
</ITEM>
Do note:
This solution flattens out to text the whole subtree rooted in a TITLE element that is a child of the top element of the XML document -- regardless of depth -- which the other posted answer doesn't.
This solution correctly flattens elements with attributes or empty elements -- which the other posted answer -- doesn't.

XSL parent selector

XML
<dsInventory>
<Room>
<RoomName>kantoor 1</RoomName>
<RoomId>1376257</RoomId>
</Room>
<Room>
<RoomName>Hal</RoomName>
<RoomId>1376258</RoomId>
</Room>
<Method>
<RoomId>1376257</RoomId>
<Name>test</Name>
</Method>
<Method>
<RoomId>1376258</RoomId>
<Name>test</Name>
</Method>
</dsInventory>
XSL
<xsl:for-each select="/dsInventory/Room">
<xsl:for-each select="/dsInventory/Method">
<xsl:if test="RoomId=../RoomId">
</xsl:if>
</xsl:for-each>
</xsl:for-each>
Problem
I'm trying to select the methods for the corresponding rooms based on the roomid. however the selector ../RoomId doesn't work. How do i get the value of the RoomId from the first for-each? Or what is the proper way to select the methods for the rooms?
Your problem here is context. The if condition is failing because it's running from the context of Method nodes, and therefore the XPath expression ../RoomID is looking for a RoomID node on dsInventory. There is no such node.
You don't say what output you want, but based on the assumption that you simply want to iterate over rooms and find the method for each, try this: (note also I use templates rather than for-each loops, which are best avoided in XSL).
You can run it and see the output at this XML Playground.
<!-- kick things off from the point of dsInventory -->
<xsl:template match="dsInventory">
<ul><xsl:apply-templates select='Room' /></ul>
</xsl:template>
<!-- room nodes -->
<xsl:template match='Room'>
<xsl:variable name='method' select='../Method[RoomId = current()/RoomId]' />
<li>
<xsl:value-of select='concat(RoomName," (",RoomId,")")' />:
method = <xsl:value-of select='concat($method/Name," (",$method/RoomId,")")' />
</li>
</xsl:template>
You havn't specified the version of XSLT you are working with or your expected output. I am going to assume you want XSLT 1.0, want to copy the document, but add the linked method name under the Room and drop the Methods.
This XSLT 1.0 style-sheet ...
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*" />
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Method"/>
<xsl:template match="Room">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
<xsl:apply-templates select="../Method[RoomId=current()/RoomId]/Name"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
... when applied to your sample input document, will produce ...
<dsInventory>
<Room>
<RoomName>kantoor 1</RoomName>
<RoomId>1376257</RoomId>
<Name>test</Name>
</Room>
<Room>
<RoomName>Hal</RoomName>
<RoomId>1376258</RoomId>
<Name>test</Name>
</Room>
</dsInventory>
Explanation
Use the current() function to specify the current item relative to the sequence constructor for the Room template, which in this case is a Room element. In the predicate we compare current()/RoomId with the RoomId of the Method step of our select expression. Thus linked Names are copied under Room. No xsl:if, no xsl:for-each, no variables. All can be achieved in the select expression using the current() function.
<xsl:for-each select="/dsInventory/Room">
<xsl:for-each select="/dsInventory/Method">
<xsl:if test="RoomId=../RoomId"></xsl:if>
</xsl:for-each>
</xsl:for-each>
As the people, who answered this question pointed out, you are trying to compare a /dsInventory/Method/RoomId to a /dsInventory/RoomId and the latter doesn't exist in the provided XML document.
I'm trying to select the methods for the corresponding rooms based on
the roomid.
Here is a complete transformation, showing how to do this:
<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="Room">
<xsl:copy>
<xsl:apply-templates/>
<xsl:apply-templates mode="grab"
select="/*/Method[RoomId = current()/RoomId]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Method" mode="grab">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="Method | Method/RoomId"/>
</xsl:stylesheet>
When this transformation is applied on the following XML document (the provided one with changed method names -- to be different from each other):
<dsInventory>
<Room>
<RoomName>kantoor 1</RoomName>
<RoomId>1376257</RoomId>
</Room>
<Room>
<RoomName>Hal</RoomName>
<RoomId>1376258</RoomId>
</Room>
<Method>
<RoomId>1376257</RoomId>
<Name>test1</Name>
</Method>
<Method>
<RoomId>1376258</RoomId>
<Name>test2</Name>
</Method>
</dsInventory>
the wanted, correct result is produced (methods added to rooms and the Method children of the top element deleted):
<dsInventory>
<Room>
<RoomName>kantoor 1</RoomName>
<RoomId>1376257</RoomId>
<Method>
<Name>test1</Name>
</Method>
</Room>
<Room>
<RoomName>Hal</RoomName>
<RoomId>1376258</RoomId>
<Method>
<Name>test2</Name>
</Method>
</Room>
</dsInventory>
Explanation:
The identity rule copies "as-is" every node for which it is selected for execution.
A template, overriding the identity template, matches Room. Here the Room element is "shallow-copied and templates are applied to its children (which selects the identity template for them and they are copied "as-is" to the output) and then templates are applied to any Method, whose RoomId child has the same value as the RoomId child of the currently matched (current()) Room element. Because we also want to later delete any Method element, here we specify that the wanted processing (different from deletion) is to be done in "grab" mode.
A second template overriding the identity template deletes all Method elements by simply matching them and having an empty body.
The template that matches a Method in mode "grab" simply shallow-copies it and applies templates to its children. Of its children the Name is matched only by the identity template and is copied "as-is". The RoomId child is matched by a more specific template than the identity template and the former is chosen for execution. The chosen template has no body, which effectively "deletes" the matched Method/RoomId from the output.

Handling array like variable with XSLT

I have declared a variable in my XSLT as given below :
<xsl:variable name="inline-array">
<Item>A</Item>
<Item>B</Item>
<Item>C</Item>
</xsl:variable>
I am accessing this variable as given below :
<xsl:param name="array"
select="document('')/*/xsl:variable[#name='inline-array']/*" />
<xsl:value-of select="$array[1]" />
This is working fine as long as my inline array has static contents. But my requirement is to dynamically assign values in the XSLT to the tag "Item" ie. Something like :
<xsl:variable name="inline-array">
<Item>$item1</Item>
<Item>$item2</Item>
<Item>$item3</Item>
</xsl:variable>
But, I tried all possible options without any luck. Any suggestions will be greatly appreciated. Any other options to fulfill my requirement is also welcome. Thanks.
One way to achieve this is to make use of an extension function, namely the node-set function, which returns a set of nodes from a result tree fragment.
First you would need to define the namespace for the extension functions like so
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
In this case, I am using the Microsoft extension functions, but others are available depending on which platform you are using. (http://exslt.org/common is another common one for non-Microsoft platforms).
Next, you define your "array" parameter (or variable, you wanted), like so.
<xsl:param name="array" select="msxsl:node-set($inline-array)"/>
Finally, you can then access this array like so
<xsl:value-of select="$array/Item[1]"/>
Putting this altogether in a simple example gives you this
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:output method="text" />
<xsl:variable name="inline-array">
<Item>
<xsl:value-of select="$Item1"/>
</Item>
<Item>
<xsl:value-of select="$Item2"/>
</Item>
<Item>
<xsl:value-of select="$Item3"/>
</Item>
</xsl:variable>
<xsl:param name="Item1">1</xsl:param>
<xsl:param name="Item2">2</xsl:param>
<xsl:param name="Item3">3</xsl:param>
<xsl:param name="array" select="msxsl:node-set($inline-array)"/>
<xsl:template match="/">
<xsl:value-of select="$array/Item[1]"/>
</xsl:template>
</xsl:stylesheet>
When run, this simply outputs the following result:
1
Firstly, are you stuck with XSLT 1.0? Workarounds like accessing the stylesheet source code using document('') are very rarely needed if only you can move to XSLT 2.0.
Secondly, I think we need to look at the design of the stylesheet, and we can't really do that without a description of the problem you are trying to solve (as distinct from your attempts at a solution.)