Is there a way to avoid processing of already processed nodes?
Input XML
<?xml version="1.0" encoding="UTF-8"?>
<root>
<node1>node1.1</node1>
<node2>node2.1</node2>
<node2>node2.2</node2>
<node1>node1.2</node1>
</root>
XSL
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="root">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="node1">
[Node1]:<xsl:value-of select="."></xsl:value-of>
<xsl:apply-templates select="following-sibling::node2"/>
[End node1]
</xsl:template>
<xsl:template match="node2">
[Node2]:<xsl:value-of select="."></xsl:value-of>
</xsl:template>
</xsl:stylesheet>
Output
<?xml version="1.0" encoding="UTF-8"?>
[Node1]:node1.1
[Node2]:node2.1
[Node2]:node2.2
[End node1]
[Node2]:node2.1
[Node2]:node2.2
[Node1]:node1.2
[End node1]
As you can see template <xsl:template match="node2"> is applied twice for every node2 element - one time from node1 template and second time when XSLT processor is transforming node2 element.
Is there any solution to avoid applying of xsl:template match="node2" second time?
I need to stop processing of node2 when I just processed it in template for node1.
Important
This example is just an emulation of more complex use case.
This means that we have additonal limitation - we can't modify template for root element processing.
I want to know if there is any way to stop processing of elements or move processing to some other elements.
You can use mode to name the template to use.
You can create an empty catch-all node that will output nothing, taking care of apply-templates calls that have no select.
The following stylesheet outputs what you need:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="root">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="node1">
[Node1]:<xsl:value-of select="."></xsl:value-of>
<xsl:apply-templates select="following-sibling::node2" mode="fromNode1"/>
[End node1]
</xsl:template>
<xsl:template match="node2" mode="fromNode1">
[Node2]:<xsl:value-of select="."></xsl:value-of>
</xsl:template>
<xsl:template match="node2"></xsl:template>
</xsl:stylesheet>
Note the empty modeless template at the end, and the added mode attribute on the template and the calling apply-templates.
This stylesheet:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text"/>
<xsl:key name="kNode2ByPrecedingNode1" match="node2"
use="generate-id(preceding-sibling::node1)"/>
<xsl:template match="root">
<xsl:apply-templates select="node1"/>
</xsl:template>
<xsl:template match="node1">
<xsl:value-of select="concat('[Node1]: ',.,'
')"/>
<xsl:apply-templates select="key('kNode2ByPrecedingNode1',
generate-id())"/>
<xsl:text>[End node1]
</xsl:text>
</xsl:template>
<xsl:template match="node2">
<xsl:value-of select="concat(' [Node2]: ',.,'
')"/>
</xsl:template>
</xsl:stylesheet>
Output:
[Node1]: node1.1
[Node2]: node2.1
[Node2]: node2.2
[End node1]
[Node1]: node1.2
[End node1]
Note: Two problems: you process node2 more than once, from root rule with applying templates to all node children, and from node1 rule; plus your following-sibling::node2 expression doesn't distinguish wich node2 follows some node1.
Edit: If you can't modify how root rule apply templates, then you would need modes for process and skip proccess:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text"/>
<xsl:key name="kNode2ByPrecedingNode1" match="node2"
use="generate-id(preceding-sibling::node1)"/>
<xsl:template match="root">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="node1">
<xsl:value-of select="concat('[Node1]: ',.,'
')"/>
<xsl:apply-templates select="key('kNode2ByPrecedingNode1',
generate-id())"
mode="output"/>
<xsl:text>[End node1]
</xsl:text>
</xsl:template>
<xsl:template match="node2"/>
<xsl:template match="node2" mode="output">
<xsl:value-of select="concat(' [Node2]: ',.,'
')"/>
</xsl:template>
</xsl:stylesheet>
XSLT doesn't track state and every apply-templates, for-each, etc will potentially produce "redundant" results, but this is entirely a problem in the design of the style-sheet - if you don't want to "process" a node more than once you need to change the appropriate templates and selects so it doesn't get handled more than once.
That would be fairly trivial for your example, but you said that your example isn't very representitive so I'd suggest you post something more complete if you're having problems.
Related
Is it possible to sort nodes as follows:
Example XML
<record>
<id>0</id>
<sku>0</sku>
<name>Title</name>
<prop>456</prop>
<number>99</number>
</record>
If I apply this template
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="record/*">
<xsl:param select="." name="value"/>
<div>
<xsl:value-of select="concat(local-name(), ' - ', $value)"/>
</div>
</xsl:template>
</xsl:stylesheet>
Ouput:
<div>id - 0</div>
<div>sku - 0</div>
<div>name - Title</div>
<div>prop - 456</div>
<div>number - 99</div>
However, I would like all 0 values to be outputted last, as so:
<div>name - Title</div>
<div>prop - 456</div>
<div>number - 99</div>
<div>id - 0</div>
<div>sku - 0</div>
Is this possible by applying a sort to the <xsl:apply-templates/>?
There is an easy way of achieving this with XSLT-1.0. Just use a predicate on xsl:apply-templates checking if the content is zero:
<xsl:template match="record/*">
<div>
<xsl:value-of select="concat(local-name(), ' - ', .)"/>
</div>
</xsl:template>
<xsl:template match="/record">
<xsl:apply-templates select="*[normalize-space(.) != '0']" />
<xsl:apply-templates select="*[normalize-space(.) = '0']" />
</xsl:template>
This does not sort the output, but groups it the way you want it. The xsl:param is unnecessary.
As far as I see your question, the issue is not any sort at all.
You even didn't write what is the specific value, which you mentioned
it the title. It is rather an arbitrary sequence of child elements of record.
Try the following script, performing just that:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:template match="record">
<xsl:copy>
<xsl:apply-templates select="name, prop, number, id, sku"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record/*">
<div><xsl:value-of select="concat(local-name(), ' - ', .)"/></div>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy>
</xsl:template>
</xsl:transform>
I used XSLT 2.0, because initially you did't specify the XSLT version.
Could you move on to version 2.0? As you can see, it allows to write quite
an elegent solution (impossible in version 1.0).
I also changed your template matching record/*. You actually don't
need any param. It is enough to use . - the value of the current
element.
Edit
Another possibility is that you want the following sort:
First, elements with non-numeric value (in your case, only name),
maybe without any sort.
Then elements with numeric value, sorted descending on this value.
If this is the case, then change the template matching record to the
following:
<xsl:template match="record">
<xsl:copy>
<xsl:apply-templates select="*[not(. castable as xs:integer)]"/>
<xsl:apply-templates select="*[. castable as xs:integer]">
<xsl:sort select="." order="descending" data-type="number"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
And add:
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
to transform tag.
But I still can't see anything, which can be called the specific value.
Considering this XML,
XML:
<?xml version="1.0" encoding="UTF-8"?>
<items>
<book>
<title>doublebell</title>
<count>available</count>
</book>
<phone>
<brand>nokia</brand>
<model></model>
</phone>
</items>
Mapping Criteria while writing XSLT:
show the newbook/newtitle only if a value is present in input.
show the newbook/newcount only if a value is present in input.
show the newphone/newbrand only if a value is present in input.
show the newphone/newmodel only if a value is present in input.
XSLT:
<?xml version="1.0" encoding="ISO-8859-1"?>
<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:variable name="book" select="items/book" />
<xsl:variable name="phone" select="items/phone" />
<xsl:template match="/">
<items>
<newbook>
<xsl:if test="$book/title!=''">
<newtitle>
<xsl:value-of select="$book/title" />
</newtitle>
</xsl:if>
<xsl:if test="$book/count!=''">
<newcount>
<xsl:value-of select="$book/count" />
</newcount>
</xsl:if>
</newbook>
<xsl:if test="$phone/brand!='' or $phone/model!=''"> <!-- not sure if this condition is required for the above mapping criteria -->
<newphone>
<xsl:if test="$phone/brand!=''">
<newbrand>
<xsl:value-of select="$phone/brand" />
</newbrand>
</xsl:if>
<xsl:if test="$phone/model!=''">
<newmodel>
<xsl:value-of select="$phone/model" />
</newmodel>
</xsl:if>
</newphone>
</xsl:if>
</items>
</xsl:template>
</xsl:stylesheet>
This is my concern:- In my actual XSLT, I have almost 70 conditions like
this, and everytime the XPath search is made twice [or thrice.. ] for
each condition [ for eg: <xsl:if test="$phone/brand!=''"> and <xsl:value-of select="$phone/brand" /> and outer if condition].
Is this much performance overhead? I don't feel it when I ran my application.
I like to hear from experienced people if this is correct way of writing the XSLT. Do I need to save the path in a variable and reuse it as done for $book
and $phone ? In such a case there will be 70+variables just to hold this.
You can approach this quite differently using templates. If you define a template that matches any element whose content is empty and does nothing:
<xsl:template match="*[. = '']" />
or possibly use normalize-space() if you want to consider elements to be empty if they contain only whitespace
<xsl:template match="*[not(normalize-space())]" />
Now with this in place add templates for the elements you are interested in
<xsl:template match="book">
<newbook><xsl:apply-templates /></newbook>
</xsl:template>
<xsl:template match="title">
<newtitle><xsl:apply-templates /></newtitle>
</xsl:template>
and so on. Now the book template will create a newbook element and go on to process its children. When it gets to the title it will have two different templates to choose from and will pick the "most specific" match. If the title is empty then the *[. = ''] template will win and nothing will be output, only if the title is non-empty will it create a newtitle element.
This way you let the template matcher do most of the work for you, you don't need any explicit conditional checks using xsl:if.
<?xml version="1.0" encoding="ISO-8859-1"?>
<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="/">
<items><xsl:apply-templates select="items/*" /></items>
</xsl:template>
<!-- ignore empty elements -->
<xsl:template match="*[not(normalize-space())]" />
<xsl:template match="book">
<newbook><xsl:apply-templates /></newbook>
</xsl:template>
<xsl:template match="title">
<newtitle><xsl:apply-templates /></newtitle>
</xsl:template>
<!-- and so on with similar templates for the other elements -->
</xsl:stylesheet>
Building on Ian's answer, you can also make a generic template that will create the "new" elements for you without having to specify each one individually. That would look like the below:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<items><xsl:apply-templates select="items/*" /></items>
</xsl:template>
<xsl:template match="*[not(normalize-space())]" />
<xsl:template match="*">
<xsl:element name="{concat('new',name())}">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
That last template just rebuilds the element by concatenating the word "new" to the front of it.
My requirement is to convert the following array
<array>
<value>755</value>
<value>5861</value>
<value>4328</value>
</array>
to this array.
<array>
<int>755</int>
<int>5861</int>
<int>4328</int>
</array>
Following is my XSLT code to do the transformation & it works. Is it the correct way because in one post I saw the use of "identity template". but I haven't used it.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/array">
<xsl:element name="array">
<xsl:for-each select="value">
<xsl:element name="int">
<xsl:value-of select="." />
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Your current method works. It is more of a "pull" style stylesheet. The "push" style uses apply-templates.
You could shorten it a bit by using element literals, which makes it a little easier to read:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/array">
<array>
<xsl:for-each select="value">
<int>
<xsl:value-of select="." />
</int>
</xsl:for-each>
</array>
</xsl:template>
</xsl:stylesheet>
A solution using the identity template and a custom template for the value element:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes"/>
<!--identity template, which copies every attribute and
node(element, text, comment, and processing instruction)
that it matches and then applies templates to all of it's
attributes and child nodes (if there are any) -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!--Specialized template that matches the value element.
Because it is more specific than the identity template above it has
a higher priority and will match when the value element is encountered.
It creates an int element and then applies templates to any attributes
and child nodes of the value element -->
<xsl:template match="value">
<int>
<xsl:apply-templates select="#*|node()"/>
</int>
</xsl:template>
</xsl:stylesheet>
You can do:
<xsl:template match="/array">
<array>
<xsl:for-each select="value">
<int>
<xsl:value-of select="." />
</int>
</xsl:for-each>
</array>
</xsl:template>
Here is a quote from the accepted answer of your link:
XSL cannot replace anything. The best you can do is to copy the parts you want to keep, then output the parts you want to change instead of the parts you don't want to keep.
That is where the identity template comes into play: it copies everything not targetted by another matching template. The upshot is, that if your base XML contains other content than just the array, then you should also include the identity template in your xslt. But if you are sure that your xml will contain no other content, then you don't need it.
I must be missing some fundamental concept of processing an XML document. Here is my source XML:
<?xml version="1.0" encoding="ISO-8859-1"?>
<Root>
<Element>visitorNameAlt</Element>
<Element>visitorScore</Element>
<Element>visitorTimeouts</Element>
<Element>Blank</Element>
<Element>homeNameAlt</Element>
<Element>homeScore</Element>
<Element>homeTimeouts</Element>
<Element>Blank</Element>
<Element>period</Element>
<Element>optionalText</Element>
<Element>flag</Element>
<Element>Blank</Element>
<Element>scoreLogo</Element>
<Element>sponsorLogo</Element>
</Root>
And my XSL stylesheet:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" indent="yes"/>
<xsl:template match="/">
<xsl:for-each select="/Root">
<xsl:value-of select="position()"/>
<xsl:value-of select="Element"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
All I want is to pluck the "Element" names from the source XML doc with their relative position in front.
My output is just "1" followed by the first element and nothing more.
I am new to XSLT, but have processed other documents successfully with for-each.
Thanks in advance.
Bill
You're looping over Root tags, not Element tags. Try this:
<xsl:template match="/">
<xsl:for-each select="/Root/Element">
<xsl:value-of select="position()"/>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
Note that you must change the second value-of select to "." or "text()".
XSLT is not an imperative programming language. The XSLT processor grabs each element in turn and tries to match it to your stylesheet. The idiomatic way to write this is without a for-each:
<xsl:template match="/Root">
<xsl:apply-templates select="Element"/>
</xsl:template>
<xsl:template match="Element">
<xsl:value-of select="position()"/>
<xsl:value-of select="."/>
</xsl:template>
The first template matches the root and tells the processor to apply the stylesheet to all the Element nodes inside the Root. The second template matches those nodes, and outputs the desired information.
I need to change namespaces in the root element as follows:
input document:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<foo xsi:schemaLocation="urn:isbn:1-931666-22-9 http://www.loc.gov/ead/ead.xsd"
xmlns:ns2="http://www.w3.org/1999/xlink" xmlns="urn:isbn:1-931666-22-9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
desired output:
<foo audience="external" xsi:schemaLocation="urn:isbn:1-931666-22-9
http://www.loc.gov/ead/ead.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-
instance" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="urn:isbn:1-931666-22-9">
I was trying to do it as I copy over the whole document and before I give any other transformation instructions, but the following doesn't work:
<xsl:template match="* | processing-instruction() | comment()">
<xsl:copy copy-namespaces="no">
<xsl:for-each select=".">
<xsl:attribute name="audience" select="'external'"/>
<xsl:namespace name="xlink" select="'http://www.w3.org/1999/xlink'"/>
</xsl:for-each>
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
Thanks for any advice!
XSLT 2.0 isn't necessary to solve this problem.
Here is an XSLT 1.0 solution, which works equally well as XSLT 2.0 (just change the version attribute to 2.0):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xlink="http://www.w3.org/1999/xlink"
exclude-result-prefixes="xlink"
>
<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="/*">
<xsl:element name="{name()}" namespace="{namespace-uri()}">
<xsl:copy-of select=
"namespace::*
[not(name()='ns2')
and
not(name()='')
]"/>
<xsl:copy-of select=
"document('')/*/namespace::*[name()='xlink']"/>
<xsl:copy-of select="#*"/>
<xsl:attribute name="audience">external</xsl:attribute>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
When the above transformation is applied on this XML document:
<foo
xsi:schemaLocation="urn:isbn:1-931666-22-9 http://www.loc.gov/ead/ead.xsd"
xmlns:ns2="http://www.w3.org/1999/xlink"
xmlns="urn:isbn:1-931666-22-9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
the wanted result is produced:
<foo xmlns="urn:isbn:1-931666-22-9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xlink="http://www.w3.org/1999/xlink"
xsi:schemaLocation="urn:isbn:1-931666-22-9 http://www.loc.gov/ead/ead.xsd"
audience="external"/>
You should really be using the "identity template" for this, and you should always have it on hand. Create an XSLT with that template, call it "identity.xslt", then into the current XSLT. Assume the prefix "bad" for the namespace you want to replace, and "good" for the one you want to replace it with, then all you need is a template like this (I'm at work, so forgive the formatting; I'll get back to this when I'm at home): ... If that doesn't work in XSLT 1.0, use a match expression like "*[namespace-uri() = 'urn:bad-namespace'", and follow Dimitre's instructions for creating a new element programmatically. Within , you really need to just apply-template recursively...but really, read up on the identity template.