Wednesday, 10 December 2008

XPath evaluation in Java using Namespaces

Earlier this year I wrote an article on testing Xpath expressions and XSL transformations in Java. This is no that hard if you know how to do it.

What I did not mention there is how to do xpath-queries on documents with namespaces.
Take for example the following xml:



<?xml version="1.0" encoding="UTF-8" ?>
<XSLBatch xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.darwin-it.nl/XMLTypes/XSLBatch ../xsd/XSLBatch.xsd"
xmlns="http://www.darwin-it.nl/XMLTypes/XSLBatch">
<XSLTransform>
<Order>1</Order>
<SourceXMLFile>/home/makker/Projects/Java/trunk/src/JavaAndXML/XMLTester/workspace/xml/sos_exportjobs.xml</SourceXMLFile>
<XSLFile>/home/makker/Projects/Java/trunk/src/JavaAndXML/XMLTester/workspace/xsl/CreateObjectType_0.2.xsl</XSLFile>
<DestinationFile>/home/makker/Projects/Java/trunk/src/JavaAndXML/XMLTester/workspace/output/sos_exportjobs.tps</DestinationFile>
</XSLTransform>
<XSLTransform>
<Order>2</Order>
<SourceXMLFile>/home/makker/Projects/Java/trunk/src/JavaAndXML/XMLTester/workspace/xml/sos_exportjobs.xml</SourceXMLFile>
<XSLFile>/home/makker/Projects/Java/trunk/src/JavaAndXML/XMLTester/workspace/xsl/CreateObjectTypeBody_0.2.xsl</XSLFile>
<DestinationFile>/home/makker/Projects/Java/trunk/src/JavaAndXML/XMLTester/workspace/output/sos_exportjobs.tpb</DestinationFile>
</XSLTransform>
</XSLBatch>


If you want to do a query on all XSLTransforms, you would probably write an Xpath of the form:
/XSLBatch/XSLTransform

But if you try to do that with the statement:

private XMLDocument xmlDoc;
...
public NodeList selectNodes(String xpath) throws XSLException {
NodeList nl = this.xmlDoc.selectNodes(xpath);
return nl;
}

then you find out that the NodeList nl would not deliver you any nodes.
This is because the XML document is of namespace: "http://www.darwin-it.nl/XMLTypes/XSLBatch" and this is not taken into account in the xpath expression above.
You could use an expression in the form of:
/xbt:XSLBatch/xbt:XSLTransform

But then: how does the parser know to what Namespace the abbreviation xbt resolves?

You could avoid the problem above with the expression:
/*[local-name()="XSLBatch"]/*[local-name()="XSLTransform"]
This is especially usefull if you have no means of specifying the namespaces you used.
But it makes the expressions very complex and if you need to query on a specific value of an attribute then you're out.

You can resolve this in Java quite easily with a NameSpace resolver. A what? A NameSpace resolver is a class that implements the oracle.xml.parser.v2.NSResolver interface. That is: using the Oracle XML parser.

A sample implementation of the Namespace Resolver is as follows:
package com.m10.xmlfiles;

import java.util.HashMap;

import oracle.xml.parser.v2.NSResolver;

public class XMLNSResolver implements NSResolver{
  private HashMap nsMap = new HashMap();
  
    public XMLNSResolver() {
    }

   public void addNS(String abbrev, String namespace){
       nsMap.put(abbrev, namespace);
   }
    public String resolveNamespacePrefix(String string) {
        return (String)nsMap.get(string);
    }
}

As you can see this implementation is fairly simple. It has a HashMap in which you can store your namespaces with an abbreviation as a key.
Since it inmplements the NSResolver interface it must implement the resolveNamespacePrefix. And that one does exactly what you might expect: it gives the namespace back that belongs to the abbreviation.

So if you do the following:
XMLNSResolver nsRes = new XMLNSResolver();
nsRes.addNS("xbt", "http://www.darwin-it.nl/XMLTypes/XSLBatch");
Then you can do your xpath query as follows:
NodeList nl = this.xmlDoc.selectNodes("/xbt:XSLBatch/xbt:XSLTransform", nsRes);

And that is how this is done.