XSLT: How to change an attribute value during <xsl:copy>?
Asked Answered
S

8

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

Salina answered 5/3, 2009 at 18:3 Comment(3)
For those who want a general solution: <xsl:stylesheet xmlns:xsl="w3.org/1999/XSL/Transform" version="1.0"> <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>Collegium
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().Remitter
@Remitter 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.Collegium
J
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.

Jonquil answered 5/3, 2009 at 18:20 Comment(9)
An alternative version would be <xsl:template match="@type[parent::property]">Spotweld
Agreed. Your way is probably more intuitive as it matches up more logically with what the template is for.Jonquil
That's what I wanted to say in the original comment as well, but forgot to actually type it. ;-)Spotweld
@Tomalak: Depends. I would prefer the parent/@type. But this is clearly subjective.Patois
property/@type is better as it is more clear and understandable. Probably even more efficient (by several microseconds :) )Maculation
Your solutiion does not work if there is a xmlns definition. For instance: <root xmlns="someurl"><property type="old"/></root>. The possible solution is <xsl:template match="node()[local-name()='property']/@*[local-name()='type']"> <xsl:attribute name="{name()}" namespace="{namespace-uri()}">new value here</xsl:attribute></xsl:template>Collegium
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.Remitter
@Remitter 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.Collegium
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.Remitter
M
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>
Maculation answered 6/3, 2009 at 3:28 Comment(4)
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.Collegium
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.Collegium
This solution does not work for me if there is a namespace definition on the root element either.Wolbrom
@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.Maculation
J
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.

Jonquil answered 5/3, 2009 at 18:20 Comment(9)
An alternative version would be <xsl:template match="@type[parent::property]">Spotweld
Agreed. Your way is probably more intuitive as it matches up more logically with what the template is for.Jonquil
That's what I wanted to say in the original comment as well, but forgot to actually type it. ;-)Spotweld
@Tomalak: Depends. I would prefer the parent/@type. But this is clearly subjective.Patois
property/@type is better as it is more clear and understandable. Probably even more efficient (by several microseconds :) )Maculation
Your solutiion does not work if there is a xmlns definition. For instance: <root xmlns="someurl"><property type="old"/></root>. The possible solution is <xsl:template match="node()[local-name()='property']/@*[local-name()='type']"> <xsl:attribute name="{name()}" namespace="{namespace-uri()}">new value here</xsl:attribute></xsl:template>Collegium
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.Remitter
@Remitter 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.Collegium
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.Remitter
C
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>
Collegium answered 13/12, 2013 at 10:16 Comment(2)
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.Boathouse
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.Collegium
P
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.

Patois answered 5/3, 2009 at 18:18 Comment(2)
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.Salina
+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).Hillary
M
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']

Mcarthur answered 7/9, 2009 at 14:39 Comment(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: https://mcmap.net/q/297034/-xslt-do-not-match-certain-attributesHypocycloid
E
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>
Expropriate answered 9/12, 2016 at 8:53 Comment(0)
S
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>
Stuff answered 5/3, 2009 at 18:21 Comment(0)
B
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.

Boathouse answered 16/12, 2013 at 4:24 Comment(2)
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 <img>'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.Collegium
And your answer is wrong if the attribute has namespace too. I don't know why you give another wrong answer after my post.Collegium

© 2022 - 2024 — McMap. All rights reserved.