13

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?

1
  • 1
    Good Question (+1). See my answer for a correct and short solution. Note that the currently accepted solution has a logical flaw (it just happens to work with your XML document, but will generally produce wrong results with other XML documents). Commented Jun 1, 2010 at 21:59

3 Answers 3

29

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.

Sign up to request clarification or add additional context in comments.

13 Comments

@iheartgreek take a very close look at the templates John has set up. Templates matching XML elements are the key to effectively using XSLT.
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.
Thanks! that makes much more sense now that I know templates have priorities! :D
+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.
@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.
|
8

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">
 <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'.

6 Comments

"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: 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: 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: 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: 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.
|
0

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

2 Comments

could you elaborate on how that's different from my solution?
@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 :-)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.