Thesaurus Rex
Thesaurus Rex

Reputation: 143

Changing an attribute value after copy

Given the following XML example file :

<root>
    <list number="123">
        <element>
            <name value="Name1"/>
            <line value="Line 1 :"/>
        </element>
        <element>
            <name value="Name1"/>
            <line value="Line 1 :"/>
        </element>
    </list>
</root>

I want to create an XSL file allowing me to replace each occurrence of Line 1 : with Line 1 : 123, the 123 coming from the list node's attribute, number.

I have the following XSL file :

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

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

    <xsl:template match="element">
        <xsl:copy-of select="."/>
        <xsl:if test="@value='Line 1 :'">
            <xsl:attribute name="type">
                <xsl:value-of select="root/list[@number]"/>
            </xsl:attribute>
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

but the transformation fails and returns something like "Attributes and namespaces nodes cannot be added to the parent element after a text, comment, pi, or sub-element node has already been added".

Any idea about what is wrong with this XSL ?

Upvotes: 1

Views: 960

Answers (1)

Mathias M&#252;ller
Mathias M&#252;ller

Reputation: 22617

Actually, the error message you get:

Attributes and namespaces nodes cannot be added to the parent element after a text, comment, pi, or sub-element node has already been added

explains the problem very well already. Your template matching element looks like this

<xsl:template match="element">
    <xsl:copy-of select="."/>
    <xsl:if test="@value='Line 1 :'">
        <xsl:attribute name="type">

The first instruction inside it:

<xsl:copy-of select="."/>

copies the element element with all its content. This includes child elements, text nodes, attributes and so on.

It is one of the rules of XSLT that once certain kinds of nodes are added to an element node, you cannot add more attribute nodes. But this is exactly what you are trying to do with

<xsl:attribute name="type">

which constructs an attribute node.

You do not actually need to xsl:copy-of the whole content of the element element, because you have an identity template in place that will copy everything anyway by default.

Only intervene in the exact spot that you would like to change. In this case, it is the value attribute of the line element - so write a template that matches exactly that.


I think what you need is the following. If not, please show the XML output you expect instead of describing it. I have assumed that there could be multiple list elements with varying number attributes.

XSLT Stylesheet

<?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"/>
    <xsl:strip-space elements="*"/>

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

    <xsl:template match="element/line/@value">
        <xsl:attribute name="value">
            <xsl:value-of select="concat(., ' ', ../../../@number)"/>
        </xsl:attribute>
    </xsl:template>

</xsl:stylesheet>

XML Output

<?xml version="1.0" encoding="utf-8"?>
<root>
   <list number="123">
      <element>
         <name value="Name1"/>
         <line value="Line 1 : 123"/>
      </element>
      <element>
         <name value="Name1"/>
         <line value="Line 1 : 123"/>
      </element>
   </list>
</root>

EDIT

A more concise requirement would be : given any XML file, how can I concatenate any attribute value inside that is equal to "Line 1 :" with the 123 from the number above ? I assume that would need more than one template...

No, this would only be a small modification. Change the second template to:

<xsl:template match="list//@*[. = 'Line 1 :']">
    <xsl:attribute name="{name()}">
        <xsl:value-of select="concat(., ' ', ancestor::list[1]/@number)"/>
    </xsl:attribute>
</xsl:template>

Upvotes: 4

Related Questions