How can I change the namespace on every node in a DOM?

16,952

Solution 1

Based on my hugely biased opinion what you want will be a huge pain in the ass. I can see the typecasting hell and numerous recursive for loops needed to do this reliably already! Ahh, Java's default implementation, how I hate your NPE:s at internals, the reversed logic, the extra steps needed for simple operations!

So yes, my suggestion would be recursive for loops with typecasting for every single possible node type, based on my very personal experience the Java's default implementation sucks that badly.

Solution 2

This is not efficient on a namespace-aware DOM. You would have to use the DOM Level 3 Core method Document.renameNode (javadoc) on every descendant Element whose namespace you wanted to change. (You wouldn't normally need to change so many Attr nodes, because the namespace of an Attr node with no prefix is always null, rather than the Element's namespace.)

If all you want to do is substitute one namespace for another, it might be quicker to use a namespace-unaware DOM, and simply change the xmlns attribute in question. You should be able to get a namespace-unaware DOM by setting the DOMConfiguration ‘namespaces’ parameter to false, but I've not tried this in Java and it's the sort of obscure little thing DOM imps would get wrong.

Solution 3

If intent is to just change name space, then just use some stream editor to change NS mapping to URL.

A Namspace is more or less a binding between namespace prefix and a URI. In order to quickly change namespace, just change the mapping:

Before: xmlns:myNS="my-namespace-uri"

After: xmlns:myNS="my-new-namespace-uri"

Basically changing mapping is sufficient, if intent is simply to change the namespace. Moreover if XML Document has default namespace, then changing the default namespace URL value would change namespace for whole of the document.

Before: xmlns="my-namespace-uri"

After: xmlns="my-new-namespace-uri"

Solution 4

How can I, given a w3c DOM (Java's default implementation, specifically) change the namespace of every element/attribute/node in that DOM? Efficiently, preferably.

I don't think there is an efficient solution that is also robust. You can't just rename something on the root element. Consider these documents:

Doc1

<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:all" xmlns:f="urn:fleet" xmlns:m="urn:mission">
  <f:starfleet>
    <m:bold>
      <f:ship name="Enterprise" />
    </m:bold>
  </f:starfleet>
</root>

Doc2

<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:all">
  <starfleet xmlns="urn:fleet">
    <bold xmlns="urn:mission">
      <ship xmlns="urn:fleet" name="Enterprise" />
    </bold>
  </starfleet>
</root>

Doc3

<?xml version="1.0" encoding="UTF-8"?>
<r:root xmlns:r="urn:all">
  <r:starfleet xmlns:r="urn:fleet">
    <r:bold xmlns:r="urn:mission">
      <r:ship xmlns:r="urn:fleet" name="Enterprise" />
    </r:bold>
  </r:starfleet>
</r:root>

These three documents are equivalent in a namespace-aware DOM. You could run the same namespaced XPath queries against any of them.

Since the DOM allows you to specify exactly how nodes should be namespaced, there is no catch-all, one-step call to change a namespace. You need to walk the DOM, taking into consideration not only prefix and URI values, but their scope at any given time.

This XSLT can be used with a Transformer to change elements namespaced as urn:fleet to be namespaced as urn:new:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:f="urn:fleet" version="1.0">
  <xsl:output method="xml" indent="yes" />
  <xsl:template match="*">
    <xsl:copy>
      <xsl:copy-of select="@*" />
      <xsl:apply-templates />
    </xsl:copy>
  </xsl:template>
  <xsl:template match="f:*">
    <xsl:variable name="var.foo" select="local-name()" />
    <xsl:element namespace="urn:new" name="{$var.foo}">
      <xsl:copy-of select="@*" />
      <xsl:apply-templates />
    </xsl:element>
  </xsl:template>
</xsl:stylesheet>

Caveats: further tweaking would be required to handle namespaced attributes; dangling urn:fleet declarations can be left behind, which is messy, but largely inconsequential; probably other stuff I haven't thought of.

Solution 5

This code given a DOM Document will return a new DOM Document in which a given set of namespace URI translations have been applied (uriMap). The keys must be the URIs in the source document, the values the replacement URIs in the destination document. Unknown namespace URIs pass through unchanged. It knows to change the value of xmlns:* attributes, but will not change other attributes that might happen to have namespace URIs as their values (e.g. XSD targetNamespace)

private static Node makeClone(Node kid, Node to, Map<String, String> uriMap) {
   Document doc = to.getNodeType() == Node.DOCUMENT_NODE ?
           (Document) to :
           to.getOwnerDocument();
   if (kid.getNodeType() == Node.ELEMENT_NODE) {
      String newURI =
              uriMap.containsKey(kid.getNamespaceURI()) ?
              uriMap.get(kid.getNamespaceURI()) :
              kid.getNamespaceURI();
      Element clone = doc.createElementNS(newURI, kid.getNodeName());
      to.appendChild(clone);
      for (int i = 0; i < kid.getAttributes().getLength(); i++) {
         Attr attr = (Attr) kid.getAttributes().item(i);
         String newAttrURI =
                 uriMap.containsKey(attr.getNamespaceURI()) ?
                 uriMap.get(attr.getNamespaceURI()) :
                 attr.getNamespaceURI();
         String newValue = attr.getValue();
         if (attr.getNamespaceURI() != null &&
                 attr.getNamespaceURI().equals(
                 "http://www.w3.org/2000/xmlns/") &&
                 uriMap.containsKey(attr.getValue()))
            newValue = uriMap.get(attr.getValue());
         clone.setAttributeNS(newAttrURI, attr.getNodeName(), newValue);
      }
      return clone;
   }
   Node clone = kid.cloneNode(false);
   doc.adoptNode(clone);
   to.appendChild(clone);
   return clone;
}

private static void copyKidsChangingNS(Node from, Node to,
        Map<String, String> uriMap) {
   NodeList kids = from.getChildNodes();
   for (int i = 0; i < kids.getLength(); i++) {
      Node kid = kids.item(i);
      Node clone = makeClone(kid, to, uriMap);
      copyKidsChangingNS(kid, clone, uriMap);
   }
}

public static Document changeDocNS(Document doc, Map<String, String> uriMap)
        throws Exception {
   DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
   dbf.setNamespaceAware(true);
   DocumentBuilder db = dbf.newDocumentBuilder();
   Document newDoc = db.newDocument();
   copyKidsChangingNS(doc, newDoc, uriMap);
   return newDoc;
}
Share:
16,952
Chris R
Author by

Chris R

I'm a software developer and inveterate geek (like many here, I suspect). For work I use so many tools I usually can't remember them all, but recently they've been heavily Python/Java. C, Java/J2EE, various scripting and release engineering tools figure heavily in the list as well.

Updated on June 17, 2022

Comments

  • Chris R
    Chris R almost 2 years

    How can I, given a w3c DOM (Java's default implementation, specifically) change the namespace of every element/attribute/node in that DOM? Efficiently, preferably. The DOM doesn't seem to have a setNamespaceURI method on it, which is inconvenient.

    I've tried XSL approaches, but they've failed to work in the JAXP transformers (although they work all right in Saxon9B, which I can't use for various other reasons).

    Basically, I need a pure core java solution that will allow me to take one document and change its namespace.

  • Chris R
    Chris R over 14 years
    I think that some of the other solutions here are more elegant, but this is what I ended up doing :)
  • Chris R
    Chris R over 14 years
    Nice idea, but at this point that'd require re-serializing to a byte stream and re-parsing, which is NOT acceptable from a performance standpoint. I have a DOM already, which I have to use.