lunedì 14 maggio 2012

Java: using XPath with namespaces and implementing NamespaceContext

http://illegalargumentexception.blogspot.it/2009/05/java-using-xpath-with-namespaces-and.html


Thursday, 28 May 2009

Java: using XPath with namespaces and implementing NamespaceContext

XPath is a handy expression language for running queries on XML. This post is about how to use it with XML namespaces in Java (javax.xml.xpath).
This Java code and uses an XPath expression to extract the value of the bar attribute from a simple document:
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();

String xml = "<data><foo bar=\"hello\" /></data>";
String value = xpath.evaluate(
"/data/foo/attribute::bar", new InputSource(
new StringReader(xml)));
System.out.println(value);
When run, it prints hello on the console.

XML with namespaces

When the XML uses namespaces, things get a little bit trickier. These two documents are functionally equivalent:
<?xml version="1.0" encoding="utf-8"?>
<!-- ns1.xml -->
<data xmlns:foo="http://foo" xmlns:bar="http://bar"
  xmlns="http://def">
  <foo:value>1</foo:value>
  <bar:value>2</bar:value>
</data>
<?xml version="1.0" encoding="utf-8"?>
<!-- ns2.xml -->
<data xmlns:bar="http://foo" xmlns:foo="http://bar"
  xmlns="http://def">
  <bar:value>1</bar:value>
  <foo:value>2</foo:value>
</data>
Note that the namespace prefixes (foo and bar) have been swapped round, but the value element in the namespace http://foo contains the value 1 in both documents. Likewise, the value element in the http://barnamespace contains the number 2 in both documents.
Since the namespace prefixes can vary in the documents, a namespaced XPath expressions need to map their own prefixes to the URIs. The namespace URIs act as constant identifiers - that's their job! In the Java API, this mapping is performed by implementing the NamespaceContextinterface.
This code uses a NamespaceContext to extract the value in the http://foo namespace from each of the documents:
InputSource ns1xml = new InputSource("ns1.xml");
InputSource ns2xml = new InputSource("ns2.xml");

NamespaceContext context = new NamespaceContextMap(
"foo", "http://foo",
"bar", "http://bar",
"def", "http://def");

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(context);
XPathExpression expression = xpath.compile("/def:data/foo:value");

System.out.println(expression.evaluate(ns1xml));
System.out.println(expression.evaluate(ns2xml));
Note that the expression was compiled for reuse. Output:
1
1
The prefixes given to the context only need to be consistent with the XPath expressions, not the documents. This code works just as well:
InputSource ns1xml = new InputSource("ns1.xml");
InputSource ns2xml = new InputSource("ns2.xml");

NamespaceContext context = new NamespaceContextMap(
"abc", "http://foo",
"pqr", "http://bar",
"xyz", "http://def");

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(context);
XPathExpression expression = xpath
.compile("/xyz:data/abc:value");

System.out.println(expression.evaluate(ns1xml));
System.out.println(expression.evaluate(ns2xml));
Unfortunately, there are no implementations of NamespaceContextprovided in the standard library (well, there is one in StAX but it is of limited utility). If you choose to implement it yourself, take note of the entire contract as defined in the javadoc. A sample implementation is provided below.
Note 2011/07: I've corrected the above listings to remove a namespace mapping of ("", "http://def") with an expression starting with /:data. This expression is not legal syntax - see the comments for more details.

Listing: NamespaceContextMap.java

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;

/**
* An implementation of <a
* href="http://java.sun.com/javase/6/docs/api/javax/xml/namespace/NamespaceContext.html">
* NamespaceContext </a>. Instances are immutable.
*
* @author McDowell
*/
public final class NamespaceContextMap implements
NamespaceContext {

private final Map<String, String> prefixMap;
private final Map<String, Set<String>> nsMap;

/**
* Constructor that takes a map of XML prefix-namespaceURI values. A defensive
* copy is made of the map. An IllegalArgumentException will be thrown if the
* map attempts to remap the standard prefixes defined in the NamespaceContext
* contract.
*
* @param prefixMappings
* a map of prefix:namespaceURI values
*/
public NamespaceContextMap(
Map<String, String> prefixMappings) {
prefixMap = createPrefixMap(prefixMappings);
nsMap = createNamespaceMap(prefixMap);
}

/**
* Convenience constructor.
*
* @param mappingPairs
* pairs of prefix-namespaceURI values
*/
public NamespaceContextMap(String... mappingPairs) {
this(toMap(mappingPairs));
}

private static Map<String, String> toMap(
String... mappingPairs) {
Map<String, String> prefixMappings = new HashMap<String, String>(
mappingPairs.length / 2);
for (int i = 0; i < mappingPairs.length; i++) {
prefixMappings
.put(mappingPairs[i], mappingPairs[++i]);
}
return prefixMappings;
}

private Map<String, String> createPrefixMap(
Map<String, String> prefixMappings) {
Map<String, String> prefixMap = new HashMap<String, String>(
prefixMappings);
addConstant(prefixMap, XMLConstants.XML_NS_PREFIX,
XMLConstants.XML_NS_URI);
addConstant(prefixMap, XMLConstants.XMLNS_ATTRIBUTE,
XMLConstants.XMLNS_ATTRIBUTE_NS_URI);
return Collections.unmodifiableMap(prefixMap);
}

private void addConstant(Map<String, String> prefixMap,
String prefix, String nsURI) {
String previous = prefixMap.put(prefix, nsURI);
if (previous != null && !previous.equals(nsURI)) {
throw new IllegalArgumentException(prefix + " -> "
+ previous + "; see NamespaceContext contract");
}
}

private Map<String, Set<String>> createNamespaceMap(
Map<String, String> prefixMap) {
Map<String, Set<String>> nsMap = new HashMap<String, Set<String>>();
for (Map.Entry<String, String> entry : prefixMap
.entrySet()) {
String nsURI = entry.getValue();
Set<String> prefixes = nsMap.get(nsURI);
if (prefixes == null) {
prefixes = new HashSet<String>();
nsMap.put(nsURI, prefixes);
}
prefixes.add(entry.getKey());
}
for (Map.Entry<String, Set<String>> entry : nsMap
.entrySet()) {
Set<String> readOnly = Collections
.unmodifiableSet(entry.getValue());
entry.setValue(readOnly);
}
return nsMap;
}

@Override
public String getNamespaceURI(String prefix) {
checkNotNull(prefix);
String nsURI = prefixMap.get(prefix);
return nsURI == null ? XMLConstants.NULL_NS_URI : nsURI;
}

@Override
public String getPrefix(String namespaceURI) {
checkNotNull(namespaceURI);
Set<String> set = nsMap.get(namespaceURI);
return set == null ? null : set.iterator().next();
}

@Override
public Iterator<String> getPrefixes(String namespaceURI) {
checkNotNull(namespaceURI);
Set<String> set = nsMap.get(namespaceURI);
return set.iterator();
}

private void checkNotNull(String value) {
if (value == null) {
throw new IllegalArgumentException("null");
}
}

/**
* @return an unmodifiable map of the mappings in the form prefix-namespaceURI
*/
public Map<String, String> getMap() {
return prefixMap;
}

}

Listing: NamespaceContextMapTest.java

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;

import org.junit.Assert;
import org.junit.Test;

import xml.NamespaceContextMap;

//JUnit 4 test
public class NamespaceContextMapTest {

@Test
public void testContext() {
Map<String, String> mappings = new HashMap<String, String>();
mappings.put("foo", "http://foo");
mappings.put("altfoo", "http://foo");
mappings.put("bar", "http://bar");
mappings.put(XMLConstants.XML_NS_PREFIX,
XMLConstants.XML_NS_URI);

NamespaceContext context = new NamespaceContextMap(
mappings);
for (Map.Entry<String, String> entry : mappings
.entrySet()) {
String prefix = entry.getKey();
String namespaceURI = entry.getValue();

Assert.assertEquals("namespaceURI", namespaceURI,
context.getNamespaceURI(prefix));
boolean found = false;
Iterator<?> prefixes = context
.getPrefixes(namespaceURI);
while (prefixes.hasNext()) {
if (prefix.equals(prefixes.next())) {
found = true;
break;
}
try {
prefixes.remove();
Assert.fail("rw");
} catch (UnsupportedOperationException e) {
}
}
Assert.assertTrue("prefix: " + prefix, found);
Assert.assertNotNull("prefix: " + prefix, context
.getPrefix(namespaceURI));
}

Map<String, String> ctxtMap = ((NamespaceContextMap) context)
.getMap();
for (Map.Entry<String, String> entry : mappings
.entrySet()) {
Assert.assertEquals(entry.getValue(), ctxtMap
.get(entry.getKey()));
}

System.out.println(context.toString());
}

@Test
public void testModify() {
NamespaceContextMap context = new NamespaceContextMap();

try {
Map<String, String> ctxtMap = context.getMap();
ctxtMap.put("a", "b");
Assert.fail("rw");
} catch (UnsupportedOperationException e) {
}

try {
Iterator<String> it = context
.getPrefixes(XMLConstants.XML_NS_URI);
it.next();
it.remove();
Assert.fail("rw");
} catch (UnsupportedOperationException e) {
}
}

@Test
public void testConstants() {
NamespaceContext context = new NamespaceContextMap();
Assert.assertEquals(XMLConstants.XML_NS_URI, context
.getNamespaceURI(XMLConstants.XML_NS_PREFIX));
Assert.assertEquals(
XMLConstants.XMLNS_ATTRIBUTE_NS_URI, context
.getNamespaceURI(XMLConstants.XMLNS_ATTRIBUTE));
Assert.assertEquals(XMLConstants.XML_NS_PREFIX, context
.getPrefix(XMLConstants.XML_NS_URI));
Assert.assertEquals(
XMLConstants.XMLNS_ATTRIBUTE_NS_URI, context
.getNamespaceURI(XMLConstants.XMLNS_ATTRIBUTE));
}

}

Nessun commento: