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:
- Remove the tag
- 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>
<开发者_如何学Goxsl: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.
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:
Works correctly when there are attributes, belonging to the old-schema namespace.
General, allows the old and new namespace to be passed as external parameters to the transformation.
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>
精彩评论