0

I am trying to replace namespace string using xslt.

I have the source namespace string and target namespace string in another xml file in the format of "namespace source="xxx" target="xxx"". All source namespace strings in my to-be-transformed xml should be changed to the corresponding target value. Only elements need to be considered as attributes don't have namespace.

I'm using the JDK default xalan xslt processor, which supports xslt 1.0. I'd like to stick to xslt 1.0 if possible, but I can change processor and use xslt 2.0 if really needed.

For example, to-be-transformed input xml:

<root xmlns="http://ns1" xmlns:ns2="http://ns2-old" xmlns:ns3="http://ns3"> 
  <ElementA xmlns="http://ns4-old">
     <ElementB/>
     <ns2:elementD/>
  </ElementA>
</root>

The output xml should be ("http://ns2-old" is changed to "http://ns2-new", and "http://ns4-old" is changed to "http://ns4-new"):

<root xmlns="http://ns1" xmlns:ns2="http://ns2-new" xmlns:ns3="http://ns3"> 
  <ElementA xmlns="http://ns4-new">
     <ElementB/>
     <ns2:elementD/>
  </ElementA>
</root>

Here is my xsl that is not working:

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:variable name="nsmapdoc" select="document('my-map-file')"/>
  <xsl:key name="nsmap" match="//namespace/@target" use="@source"/>

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

  <!-- process each element -->
  <xsl:template match="element()">
     <xsl:for-each select="namespace::*">
       <!-- for each namespace node of the element, save the string value-->
       <xsl:variable name="sourceuri"><xsl:value-of select="."/>
       </xsl:variable>

       <!-- get the target value for this namespace-->
       <xsl:variable name="targeturi">
         <xsl:for-each select="$nsmapdoc">
           <xsl:value-of select="key('nsmap', $sourceuri)"/>
         </xsl:for-each>
       </xsl:variable>

       <!-- if target value exists, replace the current namespace node string value with the target value-->
       <xsl:if test="$targeturi">
         <xsl:value-of select="$targeturi"/>
       </xsl:if>
     </xsl:for-each>
   </xsl:template>

</xsl:stylesheet>

I have a few questions:

  1. For the identity template that do the copy, why doing "match="node()|@*" instead of just "match="node()"? Is attribute also a node?

  2. I am not sure if I did correctly to get the namespace string value (like "http://ns1", "http://ns2-old") for the element.

  3. I think I got the key correctly. However, I am not sure if I used the type correctly---looks like targeturi variable is not a string. If key does not have the entry, what will lookup the entry return? In my case, I should replace the namespace value only for the namespace that has an entry in the map.

  4. How to write a new string value for the namespace node?

  5. I need to process each namespace nodes for the element. Is it the right way to define a variable inside ?

  6. please help to see what is wrong with my xsl. Any suggestion is appreciated.

user3014901
  • 367
  • 2
  • 4
  • 15
  • How is this different from the one you asked here? http://stackoverflow.com/questions/29034302/java-replace-namespace-uri-string-in-xml-file-with-xslt – michael.hor257k Apr 09 '15 at 17:32
  • This one has a xsl (not working) that I have been trying to figure out what is wrong. The other one is asking about ideas. I – user3014901 Apr 09 '15 at 17:45

1 Answers1

1

I think you have two, possibly three, separate questions here.

The first question is: how to move elements from one namespace to another, using a "map" of source-to-target namespaces. Let me answer this question first. Given:

XML

<root xmlns="http://ns1" xmlns:ns2="http://ns2-old" xmlns:ns3="http://ns3"> 
  <ElementA xmlns="http://ns4-old">
     <ElementB/>
     <ns2:ElementD/>
  </ElementA>
</root>

map.xml

<root>
    <namespace source="http://ns2-old" target="http://ns2-new"/>
    <namespace source="http://ns4-old" target="http://ns4-new"/>
</root>

The following stylesheet:

XSLT 1.0

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>

<xsl:param name="nsmapdoc" select="document('map.xml')"/>

<xsl:template match="*">
    <xsl:variable name="old-ns" select="namespace-uri()"/>
    <xsl:variable name="map-entry" select="$nsmapdoc/root/namespace[@source=$old-ns]"/>
    <xsl:variable name="new-ns">
        <xsl:choose>
            <xsl:when test="$map-entry">
                <xsl:value-of select="$map-entry/@target"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$old-ns"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:variable>
    <xsl:element name="{local-name()}" namespace="{$new-ns}">
        <xsl:copy-of select="@*"/>
        <xsl:apply-templates/>
    </xsl:element>
</xsl:template>

</xsl:stylesheet>

will return:

<?xml version="1.0" encoding="utf-8"?>
<root xmlns="http://ns1"> 
  <ElementA xmlns="http://ns4-new">
     <ElementB/>
     <ElementD xmlns="http://ns2-new"/>
  </ElementA>
</root>

Note:

  1. ElementA and its child ElementB have been moved from namespace URI "http://ns4-old" to URI "http://ns4-new";

  2. ElementD has been moved from namespace URI "http://ns2-old" to URI "http://ns2-new";

  3. The prefix of ElementD has been stripped off; this is a meaningless, cosmetic change, and it should not present any problems for the receiving application;

  4. The root element has remained in its original namespace;

  5. Unused namespace declarations have been discarded.

Note also that we are not using a key to lookup the new namespace: using a key across documents is quite awkward in XSLT 1.0 and I have chosen to do without.


The second question is how to copy the unused namespace declarations. There are several possible answers to choose from:

  1. It's not necessary to copy them, since that are not used for anything;

  2. It's not possible to copy them;

  3. It is possible to copy them by copying some element from the source document; however copying an element also copies its namespace - so this can be done only if there is a known element that is supposed to stay in its original namespace. For example, if you know beforehand that the root element is not supposed to be moved to another namespace, you can add another template to the stylesheet:

to get this result:

<?xml version="1.0" encoding="utf-8"?>
<root xmlns="http://ns1" xmlns:ns2="http://ns2-old" xmlns:ns3="http://ns3"> 
  <ElementA xmlns="http://ns4-new">
     <ElementB/>
     <ElementD xmlns="http://ns2-new"/>
  </ElementA>
</root>
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Thank you so much! I don't need to keep the old namespace, so the second question "how to copy the unused namespace declarations" is not a problem. However, I do want to "replace" the old namespace with the new namespace in place if possible. Is it doable to change the string value for the namespace node from the old namespace to the new namespace? – user3014901 Apr 09 '15 at 23:59
  • "*However, I do want to "replace" the old namespace with the new namespace in place if possible.*" I don't know if that's possible, I suspect it might not be. XSLT does not manipulate the namespaces (or anything else, for that matter) as strings. XSLT looks at the parsed XML document tree: if an element is bound to a namespace URI, then it doesn't matter how and where this binding took place in the original document. Why does this matter to you? To any XML parser down the road, the actual result reached here is indistinguishable from what you posted as your expected result. – michael.hor257k Apr 10 '15 at 00:41
  • One case gave me problem: when the element declares null namespace. For example: – user3014901 Apr 13 '15 at 23:37
  • One case gave me problem: when the element declares null namespace. For example: ' .' In this case, ElementB declares a null namespace while ElementC uses the default namespace. How to differentiate those two cases? Currently the stylesheet treats ElementB the same as ElementC, and it wrongly transforms ElementB to (the correct output needs to be . – user3014901 Apr 13 '15 at 23:50
  • "*it wrongly transforms ElementB to *" That's not what I see. – michael.hor257k Apr 14 '15 at 00:09