Java evaluate XPath against a org.w3c.dom.Node - java

Dipping my toe in a little Java at the minute and have a question about XPath.
I have a large Xml and I want to use XPath to be able to grab a specific node and then fire further XPath calls against this small chunk of Xml.
Here s rough outline of my Xml:
<Page>
<ComponentPresentations>
<ComponentPresentation>
<Component>
<Title>
<ComponentTemplate>
<ComponentPresentation>
<Component>
<Title>
<ComponentTemplate>
My first XPath selects the <Component> node based upon the value of a <ComponenTemplate> Id value:
String componentExpFormat = "/Page/ComponentPresentations/ComponentPresentation/ComponentTemplate/Id[text()='%1$s']/ancestor::ComponentPresentation";
String componentExp = String.format(componentExpFormat, template);
XPathExpression expComponent = xPath.compile(componentExp);
Node componentXml = (Node) expComponent.evaluate(xmldoc, XPathConstants.NODE);
This gives me the <Component> I want but I can;t seem to be able to then XPath against the Node:
String componentExpTitle = "/Component/Fields/item/value/Field/Name[text()='title']/parent::node()/Values/string";
XPathExpression expTitle = xPath.compile(componentExpTitle);
String eventName = expTitle.evaluate(componentXml, XPathConstants.STRING).toString();
Without this I'll have to include the full XPath each time:
/Page/ComponentPresentations/ComponentPresentation/ComponentTemplate/Id[text()='%1$s']/ancestor::ComponentPresentation/Component/Fields/item/value/Field/Name[text()='title']/parent::node()/Values/string
Is that the only way?
Cheers

An XPath expression with a leading slash
/Component/Fields/item
is absolute, and when you evaluate it with a particular context node it will start looking from the root of the document that the context node belongs to. If you remove the leading slash
Component/Fields/item
it will look for Component children of the context node.
As an aside, you can simplify those XPaths quite a bit, you don't need all the up and down the tree stuff with ancestor::, and you also don't need to use text():
componentExpFormat = "/Page/ComponentPresentations/ComponentPresentation[ComponentTemplate/Id='%1$s']";
componentExpTitle = "Component/Fields/item/value/Field[Name='title']/Values/string";

Related

How to get inner text from children XML tags using jdom2?

My XML file is structured like so:
<parent xml:space="preserve">
Hello, my name is
<variable type="firstname">ABC</variable>
and my last name is
<variable type="lastname">XYZ</variable>
</parent>
I need a way to get the text output in this format:
"Hello, my name is ABC and my last name is XYZ".
Now the issue with using jdom2 is that element.getText() method returns the entire string as a single string (with no regard to position of the child tags):
"Hello, my name is and my last name is".
Is there anyway I can get the position of the child tags/delimit them, so that even a manual variable insert can be done at some later point?
edit The example uses the Xerces parser which is included in Java runtime API for the DOM. For a JDOM2 solution see the answer from rolfl.
As a starting point you could use following snippet. Based on what you really want to achieve changes needs to be done by yourself.
xml = "<parent xml:space=\"preserve\">\n"
+ "Hello, my name is\n"
+ " <variable type=\"firstname\">ABC</variable>\n"
+ "and my last name is \n"
+ " <variable type=\"lastname\">XYZ</variable>\n"
+ "</parent>";
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = builder.parse(new ByteArrayInputStream(xml.getBytes()));
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodeList = (NodeList) xPath.compile("//parent").evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < nodeList.getLength(); i++) {
System.out.println(nodeList.item(i).getTextContent());
}
output
Hello, my name is
ABC
and my last name is
XYZ
note The snippet is not optimised. See it more as a PoC.
getText is specified in JDOM to return the immediate Text content of the element. JDOM also has the method getValue() which returns:
Returns the XPath 1.0 string value of this element, which is the complete, ordered content of all text node descendants of this element (i.e. the text that's left after all references are resolved and all other markup is stripped out.)
Applying this to your document:
Document doc = new SAXBuilder().build("parentwtext.xml");
Element root = doc.getRootElement();
System.out.println(root.getValue());
I get the output (there's an empty line at the beginning I can't show in here):
Hello, my name is
ABC
and my last name is
XYZ

xpath returns only one result

I have an xml document looks like:
<xmlList>
<Phone>
<Prefix>04</Prefix>
</Phone>
<Phone>
<Prefix>04</Prefix>
</Phone>
<Phone>
<Prefix>03</Prefix>
</Phone>
</xmlList>
I would like to retrieve the Prefix node content onlt in case it is 04.
String xml = "<xmlList><Phone><Prefix>04</Prefix></Phone><Phone><Prefix>04</Prefix></Phone><Phone><Prefix>03</Prefix></Phone></xmlList>";
XPathFactory xpathFactory = XPathFactory.newInstance();
XPath xpath = xpathFactory.newXPath();
InputSource source = new InputSource(new StringReader(xml));
// only one string is returned
String prefix = xpath.evaluate("/xmlList/Phone/Prefix", source);
Only one string is retrieved from xpath.evaluate.
I would like to get a list with all of the 04 occurences in given XML.
Possible?
As you can see in the documentation https://docs.oracle.com/javase/7/docs/api/javax/xml/xpath/XPath.html#evaluate%28java.lang.String,%20org.xml.sax.InputSource%29, that overload of the evaluate method evaluates the XPath and returns the result as string. As with XPath 1.0 the string value of a set of nodes is the string value of the first node in the node set, you get a string with the contents of the first selected node.
So you will need to use a different overload where you can specify the result type as NODESET and then you can iterate over the returned NodeList to collect the values.
Or consider to switch to an XPath 2.0 or 3.0 or XQuery 1.0 or 3.0 implementation like Saxon 9 where there are then APIs to return a sequence of strings for e.g. /xmlList/Phone/Prefix/string(). You will need to use a different API however than the JAXP XPath API which is centered around XPath 1.0.

Reading XML tag from MediaWiki using Java

I need to read output of 'search' tag from following url usign Java.
First I need to read XML into some string from following URL:
http://en.wikipedia.org/w/api.php?format=xml&action=query&list=search&srlimit=1&srsearch=big+brother
I should end up having this:
<api>
<query-continue>
<search sroffset="1"/>
</query-continue>
<query>
<searchinfo totalhits="55180"/>
<search>
<p ns="0" title="Big Brothers Big Sisters of America" snippet="<span class='searchmatch'>Big</span> <span class='searchmatch'>Brothers</span> <span class='searchmatch'>Big</span> Sisters of America is a 501(c)(3) non-profit organization whose goal is to help all children reach their potential through <b>...</b> " size="13008" wordcount="1906" timestamp="2014-04-15T06:46:01Z"/>
</search>
</query>
</api>
Then once I have the XML, I need to get content of the search tag:
Output of 'search' tag looks like this and I need to get two parts from the code in the middle:
<search>
<p ns="0" title="Big Brothers Big Sisters of America" snippet="<span class='searchmatch'>Big</span> <span class='searchmatch'>Brothers</span> <span class='searchmatch'>Big</span> Sisters of America is a 501(c)(3) non-profit organization whose goal is to help all children reach their potential through <b>...</b> " size="13008" wordcount="1906" timestamp="2014-04-15T06:46:01Z"/>
</search>
At the end, all I need is to have two strings, which would equal to this:
String title = Big Brothers Big Sisters of America
String snippet = "<span class='searchmatch'>Big..."
Can someone please help me amending this code, I am not sure what I am doing wrong. I don't think it's even retrieving XML from url, much less the tags inside the XML.
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("http://en.wikipedia.org/w/api.php?format=xml&action=query&list=search&srlimit=1&srsearch=big+brother");
doc.getDocumentElement().normalize();
XPathFactory xFactory = XPathFactory.newInstance();
XPath xpath = xFactory.newXPath();
XPathExpression expr = xpath.compile("//query/search/text()");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i=0; i<nodes.getLength();i++){
System.out.println(nodes.item(i).getNodeValue());
}
Sorry, I am a newbie and can't find the answer to this anywhere.
The main problem here is that you're asking for text nodes that are children of <search>, but in fact the <p ..> that you want is not a text node: it's an element. (In fact, the <search> element has no text node children, as you can tell when you view the response from that URL using "View Source".)
So what you want to do is change your XPath expression to
//query/search/p
which will give you the p element node. Then ask for the value of this node's two attributes title and snippet in your Java code:
Element e = (Element)(nodes.item(i));
String title = e.getAttribute("title");
String snippet = e.getAttribute("snippet");
Or, you could do two XPath queries, one for each attribute:
//query/search/p/#title
and
//query/search/p/#snippet
assuming there will only be one <p> element. If you were doing this over multiple <p> elements, you'd probably want to keep each pair of attributes together instead of having two separate lists of results.

Return type of node itself in xpath

Having the following xml
<xml>
<property href="abc">b</property>
<element attr="def">k</element>
</xml>
How can I make the following xpath return literally element.
*[#attr='def']
On it's own this might seem a weird thing to do, but using the above xpath I can't find the node type itself (only the attributes and children).
If you want the name of an element node then use name(*[#attr = 'def']) or local-name(*[#attr = 'def']).

XPath - Get id attribute from parent element

i have following xml file:
<diagnostic version="1.0">
<!-- diagnostic panel 1 -->
<panel xml:id="0">
<!-- list controls -->
<control xml:id="0_0">
<settings description="text 1"/>
</control>
<control xml:id="0_1">
<settings description="text 2"/>
</control>
</panel>
<panel xml:id="1">
<!-- list controls -->
<control xml:id="1_0">
<settings description="text 3"/>
</control>
<control xml:id="1_1">
<settings description="text 4"/>
</control>
</panel>
</diagnostic>
and definition XPath:
//*[not(#description='-')]/#description
and Java code:
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("diagnostic.xml");
XPath xpath = XPathFactory.newInstance().newXPath();
// XPath Query for showing all nodes value
XPathExpression expr = xpath.compile("//*[not(#description='-')]/#description");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(i + ": " + nodes.item(i).getParentNode() + nodes.item(i).getNodeValue());
}
This definition of XPath would return all attribute values ​​description where the value of this attribute is not '-'.
Output:
text 1
text 2
text 3
text 4
But I need to find this attribute description also attribute xml:id element control.
Output:
0_0 text 1
0_1 text 2
1_0 text 3
1_1 text 4
How to do that in my description also returns a xml:id element of control? I need to know that the description given element is control.
Someone correct me if I'm wrong, but I don't think this can be done with a single XPath expression. The concat function returns a single text result, not a list. I suggest you run multiple XPath expressions and construct your results from that, or run a single XPath expression to get the settings elements you need, then take the description attribute from it and concatenate it with the xml:id attribute from the parent element if that's a control one.
Nodes keep references to their parents. Use method getParentNode() to obtain it.
Here's an alternative: run this XPath expression...
//control[settings[#description!='-']]/#xml:id | //control/settings[#description!='-']/#description
... and then concatenate the text of the alternating results in the returned node list. In other words, text from item 0 + item 1, text from item 2 + item 3 etc.
The above XPath expression will return this node list:
0_0
text 1
0_1
text 2
1_0
text 3
1_1
text 4
You can then parse through that list and construct your results.
Be careful. This will only work if there's at most 1 settings element per control element. Also, you may find that on evaluation the XPath engine throws an error for that xml: prefix. It may say that it's unknown. You might have to bind that prefix to the correct namespace first. Since the xml prefix is reserved and bound by default to a specific namespace, this might not be needed. I'm not certain as I haven't used it before.
I've tested the expression in XMLSpy. It's not entirely impossible that the XPath engine used in Java (or the one you set for use) returns the nodes in another order. It might evaluate both parts of the "or" (the pipe symbol) separately and then dump the results into a single node list. I don't know what the XPath spec mandates regarding result ordering.
I may be just as wrong, but the nodes you traverse in the result are the XML nodes themselves. Your code sample is almost there:
- nodes.item(i) points to the attribute "description".
- nodes.item(i).getParentNode() points to the tag "settings".
- nodes.item(i).getParentNode().getParentNode() would point to the tag "control" (class Element). You could then use getAttribute() or getAttributeNS() on that node to find get the attribute you need.

Categories