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://bar
namespace 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
NamespaceContext
provided 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:
Posta un commento