• Copying XML and adding attributes in place via XSL transforms

    by  • January 14, 2013 • xml, xsl • 0 Comments

    XSL transforms are one of those things I never get used to – I spend just enough time to get up to speed with the syntax, then revisit after a few months and all that learnt knowledge has gone.

    I had a specific task today that required me to clone the XML whilst adding an attribute to nodes further down the hierarchy, which matched a specific template.

    Lets say I have the following XML document:

    Books.xml

    <?xml version="1.0" encoding="utf-8" ?>
    <Books>
      <Book Title="Title 1">
        <Authors>
          <Author Name="First Author" />
          <Author Name="Second Author" />
        </Authors>
      </Book>
      <Book Title="Title 2">
        <Authors>
          <Author Name="First Author" />
        </Authors>
      </Book>
      <Book Title="Title 3">
        <Authors>
          <Author Name="First Author" />
          <Author Name="Second Author" />
          <Author Name="Third Author" />
        </Authors>
      </Book>
    </Books>
    

    In my XSL transform, I want to keep the source XML completely intact, including any child elements the Book(s) have, but add an extra attribute to each Book element. To keep it simple, I’m going to add an attribute ID that simply outputs the Book(s) position in the XML document. To do this, I’ll use the element.

    To achieve this, my transform looks something like:

    <?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"/>
    
      <!-- matches on each book -->
      <xsl:template match="Books/Book">
        <xsl:copy>
          <xsl:attribute name="ID">
            <!-- integer position of node -->
            <xsl:number/>
          </xsl:attribute>
          <!-- identity template - copies content forward -
          This includes Authors and Author children-->
          <xsl:apply-templates select="@*|node()" />
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="@*|node()">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
      </xsl:template>
    
    </xsl:stylesheet>
    </pre>
    

    This yields the following XML:

    <?xml version="1.0" encoding="utf-8"?>
    <Books>
      <Book ID="1" Title="Title 1">
        <Authors>
          <Author Name="First Author" />
          <Author Name="Second Author" />
        </Authors>
      </Book>
      <Book ID="2" Title="Title 2">
        <Authors>
          <Author Name="First Author" />
        </Authors>
      </Book>
      <Book ID="3" Title="Title 3">
        <Authors>
          <Author Name="First Author" />
          <Author Name="Second Author" />
          <Author Name="Third Author" />
        </Authors>
      </Book>
    </Books>
    

    In which the books now have an incrementing ID, with the Title attribute, and all children intact. You can use this as the basis to add extra elements in place. It also means that you can add other templates, for example, if I wanted to add an element as a child of each author, I could do so by updating the XSL as follows:

    <?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"/>
    
      <!-- matches on each book -->
      <xsl:template match="Books/Book">
        <xsl:copy>
          <xsl:attribute name="ID">
            <!-- integer position of node -->
            <xsl:number/>
          </xsl:attribute>
          <!-- identity template - copies content forward -
          This includes Authors and Author children-->
          <xsl:apply-templates select="@*|node()" />
        </xsl:copy>
      </xsl:template>
    
      <!-- matches on each author -->
      <xsl:template match="Author">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()" />
          <node>testing</node>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="@*|node()">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
      </xsl:template>
    
    </xsl:stylesheet>
    

    which yields the following XML:

    <?xml version="1.0" encoding="utf-8"?>
    <Books>
      <Book ID="1" Title="Title 1">
        <Authors>
          <Author Name="First Author"><node>testing</node></Author>
          <Author Name="Second Author"><node>testing</node></Author>
        </Authors>
      </Book>
      <Book ID="2" Title="Title 2">
        <Authors>
          <Author Name="First Author"><node>testing</node></Author>
        </Authors>
      </Book>
      <Book ID="3" Title="Title 3">
        <Authors>
          <Author Name="First Author"><node>testing</node></Author>
          <Author Name="Second Author"><node>testing</node></Author>
          <Author Name="Third Author"><node>testing</node></Author>
        </Authors>
      </Book>
    </Books>
    

    With the <node> element added as a child of the Author, as expressed in the XSL.

    What can we do with this?

    In my case, I needed to do an initial bit of manipulation on some input XML as an intermediate step before transforming to HTML via another stylesheet through multipass stylesheets. A post for later methinks!

    Thanks go to http://stackoverflow.com/questions/2972992/xslt-how-to-add-attributes-to-copy-of for providing the original insight.

    About

    .NET developer at thetrainline.com, previously web developer at MRM Meteorite. Awarded a PhD in misbehaviour detection in wireless ad-hoc networks.A keen C# ASP.net developer bridging the gap with APIs and JavaScript frameworks, one web app at a time.

    http://www.paulkiddie.com

    Leave a Reply

    Your email address will not be published. Required fields are marked *