tags:

views:

312

answers:

3

I need an XSL solution to replace XML nodes with new nodes.

Say I have the following existing XML structure:

<root>
    <criteria>
        <criterion>AAA</criterion>
    </criteria>
</root>

And I want to replace the one criterion node with:

<criterion>BBB</criterion>
<criterion>CCC</criterion>
<criterion>DDD</criterion>

So that the final XML result is:

<root>
    <criteria>
        <criterion>BBB</criterion>
        <criterion>CCC</criterion>
        <criterion>DDD</criterion>
    </criteria>
</root>

I have tried using substring-before and substring-after to just copy the first half of the structure, then just copy the second half (in order to fill in my new nodes in between the two halves) but it appears that the substring functions only recognize text in between the nodes' tags, and not the tags themselves like I want them to. :( :(

Any other solutions?

+5  A: 

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.


Example:

<?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"/>

    <!-- This is an identity template - it copies everything
         that doesn't match another template -->
    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>

  <!-- This is the "other template". It says to use your BBB-DDD elements
       instead of the AAA element -->
  <xsl:template match="criterion[.='AAA']">
    <xsl:element name="criterion">
      <xsl:text>BBB</xsl:text>
    </xsl:element>
    <xsl:element name="criterion">
      <xsl:text>CCC</xsl:text>
    </xsl:element>
    <xsl:element name="criterion">
      <xsl:text>DDD</xsl:text>
    </xsl:element>
  </xsl:template>
</xsl:stylesheet>

The template match @* | node() matches any attribute or any other kind of node. The trick is that template matches have priorities. You can think of the rule as being "the more specific match wins". Anything is going to be more specific than "any attribute or other node". This makes the "identity" match a very low priority.

When it is matched, it simply copies any nodes it finds inside the matched attribute or node.

Any other templates you have will have a higher priority. Whatever they match, it's the code inside the more specific template that will have effect. For example, if you simply removed everything inside of the criterion[.='AAA'] template, you'd find that you had copied your input exactly, except for the "AAA" element.

John Saunders
@iheartgreek take a very close look at the templates John has set up. Templates matching XML elements are the key to effectively using XSLT.
Jweede
Thank you for providing a code example.. this is exactly what I was trying to achieve!!While this solution works for me, I really want to understand it too. I did similar things with the second template (match="criterion['AAA']") but the key part I did not know (or realize) to do was the template that copied everything that doesn't match another template. Thus.... Can you explain how the XPath match="@* | node()" achieves this? I am new to XPath and XSL, so I still don't know the ins and outs.
developer
Thanks! that makes much more sense now that I know templates have priorities! :D
developer
@Downvoter: if you have something to say, please say it with words. "-1" isn't very eloquent.
John Saunders
+1 @John Saunders - I'm guessing that you got a downvote because the template match was incorrect. It had the literal string value 'AAA' as the predicate filter, which evaluates to `true()` and therefore ensures that all `criterion` elements would be matched. You had the right idea, and a good explanation with the example. Hope you don't mind that I've corrected the predicate filter in the `@match` criteria for you.
Mads Hansen
@Mads: thank you very much. The fact that it evaluates as `true()` explains how it appeared to work when I tried it. I only wish the downvoter had explained as well as you did; it would have been possible to correct the problem immediately.
John Saunders
I have reverted my downvote, once this answer has been corrected. Thanks to @Mads-Hansen!
Dimitre Novatchev
Why are you using `<xsl:element>` to produce ... *literal-result-elements* ?
Dimitre Novatchev
Another observation: Line 3 of your stylesheet is completely unnecessary -- better removed: ` xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" `
Dimitre Novatchev
@Dimitre: I was attempting to answer the question, not to author a book on best practices in XSLT. Thank you for pointing out this trivial issue caused by the default templtate for an XSLT document in Visual Studion 2008.
John Saunders
So, Why are you using `<xsl:element>` to produce ... *literal-result-elements* ? Is this also a "trivial issue" ?
Dimitre Novatchev
Thank you all for your contributions. I actually ended up using <xsl:template match="criterion"> because I do not want to hard-code the value inside the node (the value can change).
developer
@Dimitre: to give a quick answer that illustrates the point that XSL cannot replace anything. Also to quickly illustrate the use of an identity transform. Again, I was not attempting to create a best-practice stylesheet, nor was I interested in waiting for your contribution. Feel free to post a better answer and let the best answer win. Oh, wait. You _did_ post a better answer. Sorry, I forgot.
John Saunders
This isn't correct at all: "to give a quick answer that illustrates the point that XSL cannot replace anything." ! As for the "winning" answer, be proud that an imperfect and repaired and still very far from perfect answer can "win". This is what I am fighting against -- and be sure I will downvote any future incorrect answer! So, future "contributions" like this are eagerly awaited. :)
Dimitre Novatchev
@Dimitre: excellent. And I hope you will not need to be prompted to explain your downvotes. You have helped readers a great deal by explaining this one.
John Saunders
A: 

Under the general rule of more-than-one-way-to-skin-a-cat

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output method="xml"/>
    <xsl:template match="/">
        <xsl:apply-templates />
    </xsl:template>
    <!--  
        when you capture a node with the text 'AAA'
            emit the BBB, CCC, DDD nodes
     -->
    <xsl:template match="criterion[text() = 'AAA']">
        <xsl:element name="criterion">
            <xsl:text>BBB</xsl:text>
        </xsl:element>
        <xsl:element name="criterion">
            <xsl:text>CCC</xsl:text>
        </xsl:element>
        <xsl:element name="criterion">
            <xsl:text>DDD</xsl:text>
        </xsl:element>
    </xsl:template>
    <!--  identity template  -->
    <xsl:template match="@*|node()">
      <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
      </xsl:copy>
    </xsl:template>
</xsl:stylesheet>
Dave G
@Dave: could you elaborate on how that's different from my solution?
John Saunders
@John: no difference, yours has much more explanation on it though. I think I had posted it and /then/ updated ... you just beat me to the punch :-)
Dave G
A: 

Here is one correct solution, which is probably one of the shortest:

 <xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <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="criterion[. = 'AAA']">
  <criterion>BBB</criterion>
  <criterion>CCC</criterion>
  <criterion>DDD</criterion> </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document, the wanted result is produced:

<root>
    <criteria>
        <criterion>BBB</criterion>
        <criterion>CCC</criterion>
        <criterion>DDD</criterion>
    </criteria>
</root>

Do note:

  1. The use of the identity template.

  2. How the identity template is overriden by a specific template -- only for a criterion element, whose string value is 'AAA'.

Dimitre Novatchev
"quite inaccurate"? I believe you've pointed out a single error, which Mads Hansen not only pointed out, but also explained and corrected.
John Saunders
@John-Saunders: I am glad the error was corrected. Will try to cancel my downvote. Hope that this will help you next time pay more attention. It is my principle to downvote anything that contains (at least obvious) errors. Unfortunately, I cannot afford the time to correct errors in others' answers.
Dimitre Novatchev
@Dimitre: your attitude is not helpful. I attempted to answer the question. I made a mistake. The test in my uncorrected code evaluates to `true()`. As a result, the stylesheet appeared to work. There is no reason for your "pay more attention" comment. If you cannot be bothered to particpate in the community here (which _does_ involve helping correct bad answers), then you might consider a different one, where your unhelpful contribution of a silent downvote will go unremarked.
John Saunders
@John-Saunders: I really am "bothered" to participate in the community -- especially when I see an incorrect answer hanging for hours and being copied by people that do not suspect there is a flaw in it.As for editing an answer like this -- no, to edit it will mean to completely rewrite it -- I did so by posting my answer.John, there are so many "minor" issues in your answer that I really don't have the time.
Dimitre Novatchev
@Dimitre: Mads Hansen had the time - he had to modify one line to prevent people copying a significant error. No more was required. Even if you had added a comment when you downvoted, saying there was an error so don't copy the code, that would have saved readers the tragedy of copying my code.
John Saunders
@John-Saunders: Your code had/has a number of issues and I would have needed more time to comment on them than to write and post a correct answer. Please, don't distract away from the issues in your code. Why are you using `<xsl:element>` to produce *literal-result elements* ? I have asked this yesterday and you didn't correct this in your answer!
Dimitre Novatchev