67

I have an XML document, and I want to change the values for one of the attributes.

First I copied everything from input to output using:

<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
</xsl:template>

And now I want to change the value of the attribute "type" in any element named "property".

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
tomato
  • 5,644
  • 13
  • 43
  • 48
  • 1
    For those who want a general solution: some new value here – astonia Dec 17 '13 at 08:06
  • 2
    Your solution is needlessly verbose, and partially wrong. There should be '`http://www.`' at the beginning of the `xsl` namespace. Also, matching/selecting `node()|comment()|processing-instruction()|text()` is superfluous, as comments, processing instructions and text nodes are matched by `node()`. – Flynn1179 Dec 17 '13 at 08:56
  • @Flynn1179 My solution works well for all situations. I don't know why http:// is missing after copy/paste, that's a mistake, thank you for pointing out. I just gave a possible solution, not the perfect one. The most important thing is that my solution works for almost all situations though "it's superfluous" as you said. While on the other hand, most of other answers including the one that "the xslt expert" gave do not work at all. But they did not admit that. – astonia Dec 18 '13 at 02:26

8 Answers8

65

This problem has a classical solution: Using and overriding the identity template is one of the most fundamental and powerful XSLT design patterns:

<xsl:stylesheet version="1.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <xsl:param name="pNewType" select="'myNewType'"/>

    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="property/@type">
        <xsl:attribute name="type">
            <xsl:value-of select="$pNewType"/>
        </xsl:attribute>
    </xsl:template>
</xsl:stylesheet>

When applied on this XML document:

<t>
  <property>value1</property>
  <property type="old">value2</property>
</t>

the wanted result is produced:

<t>
  <property>value1</property>
  <property type="myNewType">value2</property>
</t>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • 1
    This solution does not work if there is a namespace definition. I have written a comment some days ago, and the answer's writer replied. But they are gone now, so I have to repost the comment to those who come here not to be misguided by those wrong answers, especially by those writers who tended to be misguiding. – astonia Dec 17 '13 at 03:10
  • Maybe you are too focusing on theory instead of the problem itself. Google took me here, your answer is helpful, but cannot solve my problem at all. So I finally got a better one whatever it's theoretically right or wrong, or may cause some one crazy about namespaces something. What I do care is to find a way to solve my problem and I hope my experience may help other people who have similiar situations. Your answer is really helpful, and you are really an enthusiastic answerer here. But I have to say, the solution you gave for this question does not work at all. – astonia Dec 17 '13 at 06:50
  • This solution does not work for me if there is a namespace definition on the root element either. – a2f0 Jun 14 '15 at 23:09
  • 2
    @dps Your problem is orthogonal (unrelated) to this question. And your problem is the most FAQ about XPath. Just search for "XPath default namespace" and you'll find probably hundreds of good answers and explanations. – Dimitre Novatchev Jun 14 '15 at 23:16
41

Tested on a simple example, works fine:

<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
</xsl:template>
<xsl:template match="@type[parent::property]">
  <xsl:attribute name="type">
    <xsl:value-of select="'your value here'"/>
  </xsl:attribute>
</xsl:template>

Edited to include Tomalak's suggestion.

Welbog
  • 59,154
  • 9
  • 110
  • 123
  • 1
    An alternative version would be – Tomalak Mar 05 '09 at 18:36
  • Agreed. Your way is probably more intuitive as it matches up more logically with what the template is for. – Welbog Mar 05 '09 at 18:50
  • 1
    That's what I wanted to say in the original comment as well, but forgot to actually type it. ;-) – Tomalak Mar 05 '09 at 18:56
  • 1
    @Tomalak: Depends. I would prefer the parent/@type. But this is clearly subjective. – Richard Mar 05 '09 at 20:25
  • 10
    property/@type is better as it is more clear and understandable. Probably even more efficient (by several microseconds :) ) – Dimitre Novatchev Mar 06 '09 at 03:19
  • Your solutiion does not work if there is a xmlns definition. For instance: . The possible solution is new value here – astonia Dec 13 '13 at 10:10
  • Actually, a quicker 'fix' for that would be to add `xmlns:doc="someurl"` to the `xsl:stylesheet` element, and change the match to `@type[parent::doc:property]` or `doc:property/@type`. Of course this does depend on you knowing there's a namespace when you write your stylesheet. Unless of course you can get away with just matching on `@type`- attributes don't have namespaces. – Flynn1179 Dec 13 '13 at 19:38
  • @Flynn1179 Unfortunately, there are many situations that xmlns is undetermined when writing the xslt. So your quicker fix would not solve those real world problems either just as the original answer does. – astonia Dec 17 '13 at 03:05
  • 1
    Perhaps, but those situations are mercifully rare. Given that the OP never specified that there were any namespaces involved, it's perhaps a bit uncharitable to describe an answer that doesn't consider them as 'wrong'. However, a more 'complete' answer for the benefit of any other interested parties probably could include a 'this only works if there's no namespaces' caveat, but this is by no means necessary to fully answer the question as it was asked. – Flynn1179 Dec 17 '13 at 09:00
12

The top two answers will not work if there is a xmlns definition in the root element:

<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
    <property type="old"/>
</html>

All of the solutions will not work for the above xml.

The possible solution is like:

<?xml version="1.0"?> 

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <xsl:output omit-xml-declaration="yes" indent="yes"/>
  <xsl:template match="node()[local-name()='property']/@*[local-name()='type']">
      <xsl:attribute name="{name()}" namespace="{namespace-uri()}">
                some new value here
          </xsl:attribute>
  </xsl:template>

  <xsl:template match="@*|node()|comment()|processing-instruction()|text()">
      <xsl:copy>
          <xsl:apply-templates select="@*|node()|comment()|processing-instruction()|text()"/>
      </xsl:copy>
  </xsl:template>
</xsl:stylesheet>
astonia
  • 528
  • 6
  • 9
  • You are making this much more complicated than it needs to be. I have posted an answer that shows how to make those top two answers work in your situation. – michael.hor257k Dec 16 '13 at 04:28
  • Your answer is much more complicated than mine. I cannot see why you give the extra answer after my post. What you should do is to plus my answer. And frankly speaking, your answer is wrong if the attribute has a namespace too. – astonia Dec 17 '13 at 03:30
5

You need a template that will match your target attribute, and nothing else.

<xsl:template match='XPath/@myAttr'>
  <xsl:attribute name='myAttr'>This is the value</xsl:attribute>
</xsl:template>

This is in addition to the "copy all" you already have (and is actually always present by default in XSLT). Having a more specific match it will be used in preference.

Richard
  • 106,783
  • 21
  • 203
  • 265
  • I've tried it without the "copy all" part and it only got what was between the tags. None of the tag themselves or the attributes got copied. – tomato Mar 05 '09 at 19:32
  • +1 because of its simplicity and because this will work for both the use case presented, and much more complex xpaths where you only want to change the attribue on an element at a very specific xpath (which is what I was looking for when I came to this page). – TMWP Jul 06 '20 at 22:45
2

I had a similar case where I wanted to delete one attribute from a simple node, and couldn't figure out what axis would let me read the attribute name. In the end, all I had to do was use

@*[name(.)!='AttributeNameToDelete']

rwrobson
  • 31
  • 1
  • 1
    +1 because this construct is useful if one wants to change an attribute within a copy. but the answer is incomplete. See this answer for what I mean: http://stackoverflow.com/a/12919373/520567 – akostadinov Oct 17 '12 at 08:02
2

I also came across same issue and i solved it as follows:

<!-- identity transform -->
<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
</xsl:template>

<!-- copy property element while only changing its type attribute -->
<xsl:template match="property">
  <xsl:copy>
    <xsl:attribute name="type">
      <xsl:value-of select="'your value here'"/>
    </xsl:attribute>
    <xsl:apply-templates select="@*[not(local-name()='type')]|node()"/>
  </xsl:copy>
</xsl:template>
g t
  • 7,287
  • 7
  • 50
  • 85
Ashish Lahoti
  • 648
  • 6
  • 8
1

If your source XML document has its own namespace, you need to declare the namespace in your stylesheet, assign it a prefix, and use that prefix when referring to the elements of the source XML - for example:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xhtml="http://www.w3.org/1999/xhtml">

<xsl:output method="xml" encoding="utf-8" indent="yes" omit-xml-declaration="yes" />

<!-- identity transform -->
<xsl:template match="node()|@*">
    <xsl:copy>
        <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
</xsl:template>

<!-- exception-->    
<xsl:template match="xhtml:property/@type">
    <xsl:attribute name="type">
        <xsl:text>some new value</xsl:text> 
    </xsl:attribute>
</xsl:template>

</xsl:stylesheet>

Or, if you prefer:

...
<!-- exception-->    
<xsl:template match="@type[parent::xhtml:property]">
  <xsl:attribute name="type">
        <xsl:text>some new value</xsl:text> 
  </xsl:attribute>
</xsl:template>
...

ADDENDUM: In the highly unlikely case where the XML namespace is not known beforehand, you could do:

<?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" encoding="utf-8" indent="yes" omit-xml-declaration="yes" />

<!-- identity transform -->
<xsl:template match="node()|@*">
    <xsl:copy>
        <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
</xsl:template>

<!-- exception -->
<xsl:template match="*[local-name()='property']/@type">
    <xsl:attribute name="type">
        <xsl:text>some new value</xsl:text> 
    </xsl:attribute>
</xsl:template>

Of course, it's very difficult to imagine a scenario where you would know in advance that the source XML document contains an element named "property", with an attribute named "type" that needs replacing - but still not know the namespace of the document. I have added this mainly to show how your own solution could be streamlined.

michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • 1
    The unknown namespace scenario is not unlikely case. At least you can write one xslt to handle all xml regardless what their namespaces are. For ex, I need to transform 's src attribute to an empty picture for the pages of thousands of websites crawled from internet. Obviosly, their namespace definitions are undetermined. And each time you join a new project if xslt is needed, the general template can be one of your base toolkit. You don't have to change the namespace for different projects. – astonia Dec 17 '13 at 03:24
  • And your answer is wrong if the attribute has namespace too. I don't know why you give another wrong answer after my post. – astonia Dec 17 '13 at 03:31
1

For the following XML:

<?xml version="1.0" encoding="utf-8"?>
<root>
    <property type="foo"/>
    <node id="1"/>
    <property type="bar">
        <sub-property/>
    </property>
</root>

I was able to get it to work with the following XSLT:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="//property">
        <xsl:copy>
            <xsl:attribute name="type">
                <xsl:value-of select="@type"/>
                <xsl:text>-added</xsl:text>
            </xsl:attribute>
            <xsl:copy-of select="child::*"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>
Andrew Hare
  • 344,730
  • 71
  • 640
  • 635