vocaro
vocaro

Reputation: 2779

XSLT identity transform produces wrong namespace

I have a custom XML schema that is evolving: elements are added, others deleted, and the namespace changes to reflect the new version (e.g., from "http://foo/1.0" to "http://foo/1.1"). I want to write an XSLT that converts XML documents from the old schema to the new one. My first attempt works, but it's verbose and unscalable, and I need help refining it.

Here's a sample document for the 1.0 schema:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<foo:rootElement xmlns:foo="http://foo/1.0">
    <obsolete>something</obsolete>
    <stuff>
        <alpha>a</alpha>
        <beta>b</beta>
    </stuff>
</foo:rootElement>

In the 1.1 schema, the "obsolete" element goes away but everything else remains. So the XSLT needs to do two things:

  1. Remove the tag
  2. Change the namespace from http://foo/1.0 to http://foo/1.1

Here's my solution:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:foo1="http://foo/1.0"
    xmlns:foo="http://foo/1.1" 
    exclude-result-prefixes="foo1">

    <xsl:output method="xml" standalone="yes" indent="yes"/>

    <!-- Remove the "obsolete" tag -->    
    <xsl:template match="foo1:rootElement/obsolete"/>

    <!-- Copy the "alpha" tag -->
    <xsl:template match="foo1:rootElement/stuff/alpha">
        <alpha>
            <xsl:apply-templates/>
        </alpha>
    </xsl:template>

    <!-- Copy the "beta" tag -->
    <xsl:template match="foo1:rootElement/stuff/beta">
        <beta>
            <xsl:apply-templates/>
        </beta>
    </xsl:template>

    <!-- Copy the "stuff" tag -->
    <xsl:template match="foo1:rootElement/stuff">
        <stuff>
            <xsl:apply-templates/>
        </stuff>
    </xsl:template>

    <!-- Copy the "rootElement" tag -->
    <xsl:template match="foo1:rootElement">
        <foo:rootElement>
            <xsl:apply-templates/>
        </foo:rootElement>
    </xsl:template>

</xsl:stylesheet>

Although this produces the output I want, notice that I have a copying template for every element in the schema: alpha, beta, etc. For complex schemas with hundreds of kinds of elements, I'd have to add hundreds of templates!

I thought I could eliminate this problem by reducing all those copying templates into a single identity transform, like this:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:foo1="http://foo/1.0"
    xmlns:foo="http://foo/1.1" 
    exclude-result-prefixes="foo1">

    <xsl:output method="xml" standalone="yes" indent="yes"/>

    <xsl:template match="foo1:rootElement/obsolete"/>

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

    <xsl:template match="foo1:rootElement/stuff">
        <xsl:copy>
            <xsl:call-template name="identity"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="foo1:rootElement">
        <foo:rootElement>
            <xsl:apply-templates/>
        </foo:rootElement>
    </xsl:template>

</xsl:stylesheet>

But it produces:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<foo:rootElement xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:foo="http://foo/1.1">  
    <stuff xmlns:foo="http://foo/1.0">
      <stuff>
        <alpha>a</alpha>
        <beta>b</beta>
      </stuff>
   </stuff>
</foo:rootElement>

The "stuff" element was copied, which is what I want, but it's wrapped in another "stuff" element, tagged with the old namespace. I don't understand why this is happening. I've tried many ways of fixing this but have been unsuccessful. Any suggestions? Thanks.

Upvotes: 2

Views: 1483

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243449

This transformation:

<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:param name="pOldNS" select="'http://foo/1.0'"/>
 <xsl:param name="pNewNS" select="'http://foo/1.1'"/>

 <xsl:template match="/*">
  <xsl:element name="{name()}" namespace="{$pNewNS}">
   <xsl:apply-templates select="node()|@*"/>
  </xsl:element>
 </xsl:template>

 <xsl:template match="*">
  <xsl:element name="{name()}">
   <xsl:copy-of select="namespace::*[not(.=$pOldNS)]"/>

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

 <xsl:template match="@*">
  <xsl:choose>
   <xsl:when test="namespace-uri()=$pOldNS">
    <xsl:attribute name="{name()}" namespace="{$pNewNS}">
     <xsl:value-of select="."/>
    </xsl:attribute>
   </xsl:when>
   <xsl:otherwise>
    <xsl:copy-of select="."/>
   </xsl:otherwise>
  </xsl:choose>
 </xsl:template>

 <xsl:template match="obsolete"/>
</xsl:stylesheet>

when applied on this XML document:

<foo:rootElement xmlns:foo="http://foo/1.0">
    <obsolete>something</obsolete>
    <stuff>
        <alpha x="y" foo:t="z">a</alpha>
        <beta>b</beta>
    </stuff>
</foo:rootElement>

produces the wanted, correct result (obsolute elements deleted, namespace upgraded, attribute nodes correctly processed, including nodes that were in the old namespace):

<foo:rootElement xmlns:foo="http://foo/1.1">
   <stuff>
      <alpha x="y" foo:t="z">a</alpha>
      <beta>b</beta>
   </stuff>
</foo:rootElement>

Main advantages of this solution:

  1. Works correctly when there are attributes, belonging to the old-schema namespace.

  2. General, allows the old and new namespace to be passed as external parameters to the transformation.

Upvotes: 2

Gunther
Gunther

Reputation: 5256

While you might want to copy the attributes, elements need to be regenerated in the new namespace. So I'd suggest to do it like this:

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

    <xsl:template match="foo1:rootElement/obsolete"/>

    <xsl:template match="attribute()">
      <xsl:copy/>
    </xsl:template>

    <xsl:template match="element()">
      <xsl:variable name="name" select="local-name()"/>
      <xsl:element name="{$name}" namespace="http://foo/1.1">
        <xsl:apply-templates select="node()|@*"/>
      </xsl:element>
    </xsl:template>

</xsl:stylesheet>

Upvotes: 2

Related Questions