Is this the right way to use javax.xml.xpath?

L

Larry Coon

I'm trying to figure out the right way to use the xpath api
under 1.5. I'm not sure I'm doing it the right way when a
node has multiple children -- I can get to the data, but my
technique seems kludgy. The samples I found online tell you
how to get a NodeList, but don't really help you from there.

First, here is a sample document (XPathTest.xml):

<?xml version="1.0" encoding="utf-8"?>

<test>
<one>This is a single item</one>

<many>
<item>This is one of many items #1</item>
<item>This is one of many items #2</item>
<item>This is one of many items #3</item>
</many>
</test>

-----

Here's an SSCCE. The technique I use to get the single item
is easy enough, but I start to scratch my head when I go after
the many items:

import java.io.*;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
import org.w3c.dom.*;

public class TestXPath {
public TestXPath(String configFileName) {
try {
DocumentBuilderFactory domFactory =
DocumentBuilderFactory.newInstance();

DocumentBuilder builder = domFactory.newDocumentBuilder();
Document document = builder.parse(new File(configFileName));

XPathFactory xFactory = XPathFactory.newInstance();
XPath xPath = xFactory.newXPath();

String s1 = xPath.evaluate("/test/one", document);
System.out.println("One item: " + s1);

NodeList nodes = (NodeList)
xPath.evaluate("/test/many/item",
document, XPathConstants.NODESET);

int length = nodes.getLength();
System.out.println("Number of items: " + length);

for (int i = 0; i < length; i++) {
String s2 = xPath.evaluate("/test/many/item["
+ (i + 1) + "]", document);

System.out.println("Many items #" + i + ": " + s2);
}
}
catch (Exception x) { x.printStackTrace(); }
}

public static void main(String[] args) {
new TestXPath("XPathTest.xml");
}
}

-----

I realize that a NodeList isn't Iterable, so I need to use a for loop
to get the individual items. But the only thing I'm using the NodeList
for is to get a count -- I go back to the DOM to retrieve the actual
data. This doesn't seem right. Should I be using XPath on the NodeList
to get at the data more directly? I tried various ways, and didn't get
anything to work. Any advice?
 
C

Chris Smith

Larry Coon said:
I'm trying to figure out the right way to use the xpath api
under 1.5. I'm not sure I'm doing it the right way when a
node has multiple children -- I can get to the data, but my
technique seems kludgy. The samples I found online tell you
how to get a NodeList, but don't really help you from there.

Once you've got a NodeList, you would typically fall back to the normal
DOM API to extract the data from the node. If you need (or just want)
to perform further XPath queries, you can pass the Node from the
NodeList as context. For example...

public class TestXPath
{
public static void main(String[] args) throws Exception
{
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
DocumentBuilder b = f.newDocumentBuilder();
Document d = b.parse(new InputSource(new ByteArrayInputStream(
"<test><many><item>a</item><item>b</item></many></test>"
.getBytes("US-ASCII"))));

XPathFactory xf = XPathFactory.newInstance();
XPath xp = xf.newXPath();

NodeList nl = (NodeList) xp.evaluate(
"/test/many/item", d, XPathConstants.NODESET);

for (int i = 0; i < nl.getLength(); i++)
{
Node n = nl.item(i);
System.out.println(xp.evaluate(".", n));
}
}
}
 
L

Larry Coon

Chris said:
Once you've got a NodeList, you would typically fall back to the normal
DOM API to extract the data from the node.

Thanks for the reply, Chris. This is one of the things that made
me want to ask -- falling back on the DOM API once I got to a certain
point seemed really inelegant. I assumed it _couldn't_ be the right
way to do it.
If you need (or just want)
to perform further XPath queries, you can pass the Node from the
NodeList as context. For example...

(example snipped)

Ah, I tried various ways of doing evalutate() on the node, but I
never used the expression ".". I guess that warrants a "duh."

What I'd really like to do is eliminate the iteration altogether.
The contents are being used to populate a Collection, so I'd love
to be able to just make one call to addAll(). Is there a set-based
XPath operation that I can use as the argument to addAll()?
 
C

Chris Smith

Larry Coon said:
What I'd really like to do is eliminate the iteration altogether.
The contents are being used to populate a Collection, so I'd love
to be able to just make one call to addAll(). Is there a set-based
XPath operation that I can use as the argument to addAll()?

Not that I can see. However, you can write the following once, in a
utility lib somewhere:

public class Nodes
{
public static List<Node> asList(final NodeList nl)
{
return new AbstractList<Node>() {
@Override
public int size()
{
return nl.getLength();
}

@Override
public Node get(int index)
{
return nl.item(index);
}
};
}
}

And,

col.addAll(Nodes.asList(xpath.evaluate(...),
XPathConstants.NODESET)));

Is that close to what you want? Feel free to drop the @Override and
<Node> from the code above if you aren't using Java 5.0 for the project.
 
L

Larry Coon

Chris said:
Not that I can see. However, you can write the following once, in a
utility lib somewhere:

(snipped)

Ah, that makes perfect sense -- thanks!
 
L

Larry Coon

Chris said:
Not that I can see. However, you can write the following once, in a
utility lib somewhere:

public class Nodes
{
public static List<Node> asList(final NodeList nl)
{
return new AbstractList<Node>() {
@Override
public int size()
{
return nl.getLength();
}

@Override
public Node get(int index)
{
return nl.item(index);
}
};
}
}

And,

col.addAll(Nodes.asList(xpath.evaluate(...),
XPathConstants.NODESET)));

Is that close to what you want?

On second look, it isn't quite what I want. I don't want to populate
a collection of Nodes, I want to populate a collection of Strings. I
suppose the approach is to send the XPath to asList(), so get() can use
it to evaluate() the node and return the resulting String?
 
C

Chris Smith

Larry Coon said:
On second look, it isn't quite what I want. I don't want to populate
a collection of Nodes, I want to populate a collection of Strings. I
suppose the approach is to send the XPath to asList(), so get() can use
it to evaluate() the node and return the resulting String?

Sure, that'll work.

Let me clarify something: I used the XPath instance to extract the text
from a node in my first reply only because it is more general. I
assumed that in practice you may want to perform more operations to
which XPath is well-suited. XPath is a way to find your way around an
XML file; it's not a replacement for DOM. If all you want is the text
of a node, just call getTextContent() on the Node, and you're done.
XPath is good for navigation, which is clumsy and painful in the DOM,
but XPath is clumsy and painful for direct operations on a single node
of an XML document.

In a less trivial example where XPath is really appropriate, it would
perhaps still be cleaner for the implementation of Nodes.asList to
construct its own XPath instance, since there's no reason to assume that
the original NodeList comes from XPath code and therefore no reason to
assume that the caller has an XPath instance to supply.

Also, you'll probably want to rename my Nodes.asList to something else,
since it no longer does what the name suggests.

If you again want to be somewhat general, you can write:

public static interface Filter<S,T>
{
public T exec(S arg);
}

public static <S,T> List<T> mapList(List<S> source, Filter<S,T> f)
{
List<T> r = new ArrayList<T>(source.size());
for (S from : source) r.add(f.exec(from));
return r;
}

and then,

col.addAll(MyUtil.mapList(
Nodes.asList(xpath.evaluate(...)),
new Filter<Node,String>() {
public String exec(Node arg)
{
return arg.getTextContent();
}
}));
 
L

Larry Coon

Chris said:
Sure, that'll work.

Let me clarify something:

(clarification snipped)

Thanks Chris, I appreciate all the help.


Larry Coon
University of California
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,054
Latest member
TrimKetoBoost

Latest Threads

Top