2

I have the following c# method, that performs some operation on all nodes reachable from refNode, through xpath

void foo(XmlNode refNode, string xpath)
{
    XmlNodeList list=refNode.SelectNodes(xpath);
    //perform operation on each element of the list
}

One of the input xml that i'm getting is:

<A>
    <B>***
        <C>
                  <B>One</B>
            </C>
        <B>
                  <B>Two</B>
            </B>
    </B>
    <B>...</B>
    <B>...</B>
</A>

where i need to select a refNode <B> (marked ***) and pass it to foo() with an xpath that selects all descendent <B> nodes of refNode, but not nested inside any other <B> node

for example in the given input the result should contain:

1. <B>One</B>
2. <B><B>Two</B></B>

I have tried .//B which gives me 3 results and .//B[not(ancesotr::B)] which returns 0 results.

What Xpath should I use to get the desired result?

Edit

I can make changes to method foo, but not its signature. This method is a part of a library and is being used by few users. The input given above is just a specific instance, the user might also send node A as refnode and ask for all C nodes.

Edit2 @Dimitre Novatchev's solution works for me if I can get the xpath of the refnode inside foo without changing its signature or if there is some way to specify this node, i.e. the node on which the xpath is being applied.

.//B[not(ancesotr::B) or ancesotr::B[1]=**this**]
Amit Khanna
  • 489
  • 4
  • 16

4 Answers4

2

Use this pure XPath 1.0 expression:

$vrefNode/descendant::B[count(ancestor::B) - count($vrefNode/ancestor::B) = 1]

where $vrefNode needs to be substituted (unless you can use variable references) with the XPath expression that selects the "reference node".

XSLT - based verification:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/">
  <xsl:copy-of select=
  "/*/B[1]/descendant::B[count(ancestor::B) - count(/*/B[1]/ancestor::B) = 1]"/>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<A>
    <B>***
        <C>
            <B>One</B>
        </C>
        <B>
            <B>Two</B>
        </B>
    </B>
    <B>...</B>
    <B>...</B>
</A>

the XPath expression is evaluated and the selected elements by this evaluation are copied to the output:

<B>One</B>
<B>

   <B>Two</B>

</B>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • thanks for your reply, it is not possible to get the xpath of refnode while calling the function. Is there some way to get it inside foo() from refNode? – Amit Khanna Dec 30 '12 at 06:22
  • @AmitKhanna, Define `foo()` as: `void foo(XmlNode refNode, string xpath, string refNodeXpath)` and pass as the last argument the XPath expression that selects `refNode`. I am going to sleep shortly -- will be able to answer future questions in about 8hours. Goodnight. – Dimitre Novatchev Dec 30 '12 at 06:33
  • cant do that since its a public method and others are already using it. cant change its signature. – Amit Khanna Dec 30 '12 at 06:38
  • @AmitKhanna, Then add a new method to the class, that has the new argument -- there is no problem with that. – Dimitre Novatchev Dec 30 '12 at 16:01
  • that will solve the problem still i dont think that will be a good solution form design point of view. I'll end up in having two methods that perform the same job, confusing the user which one to use. Also refnodexpath looks redundant to me as user is already passing the node. Maybe, the user also doesn't have this value himself, they might also be getting an XmlNode in their code. One way i've thought of is to construct refnodexpath inside foo by traversing parents of refnode till root and adding name of each node in the path. – Amit Khanna Jan 01 '13 at 08:16
  • 1
    @AmitKhanna, Yes, See this answer: http://stackoverflow.com/questions/4746299/generate-get-xpath-from-xml-node-java for a way to build the XPath expression for a node -- even in XSLT. – Dimitre Novatchev Jan 01 '13 at 16:03
0

I am not sure pure XPath can do that. XQuery has more expressive power, for instance you can easily use let to hold the B element whose subtree you search for further B descendants in a variable and then you can make sure it is the only B ancestor. So here is an XQuery example:

let $inp := <A>
  <B>
    ***
    <C>
      <B>One</B>
    </C>
    <B>
      <B>Two</B>
    </B>
    <D>
      <E>
        <B>Three</B>
      </E>
    </D>
    <D>
      <B>Four<Foo>
        <B>Five</B>
      </Foo>
    </B>
    </D>
  </B>
  <B>...</B>
  <B>...</B>
</A>
let $b := $inp/B[1]
return $b/descendant::B[count(ancestor::B) = 1 and ancestor::B[1] is $b]

That finds the nodes

<B>One</B>
<B>
<B>Two</B>
</B>
<B>Three</B>
<B>Four<Foo>
<B>Five</B>
</Foo>
</B>

There are XQuery implementations for the .NET framework like XmlPrime, Saxon 9 .NET version, AltovaXML or http://qm.codeplex.com/.

Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
0

an xpath that selects all descendent <B> nodes of refNode, but not nested inside any other <B> node

In XSLT you could use

.//B[generate-id(ancestor::B[1]) = generate-id(current())]

This will select all those B descendants of the current context node whose nearest enclosing B ancestor is the current context node itself. But this relies on generate-id() and current() which are XSLT-specific, and not part of plain XPath.

The following would be a two-step alternative:

foo(refNode, ".//B[count(ancestor::B) = " + refNode.SelectNodes("ancestor-or-self::B").Count + "]");

generating the XPath expression dynamically. This will find all B descendants of refNode that have the same number of B ancestors as refNode (plus one, if refNode is itself a B element), which has the effect of excluding B descendants that are more than one "layer of B's" deep.

Ian Roberts
  • 120,891
  • 16
  • 170
  • 183
0

Do you have to use XPath? If you can use Linq-to-XML,

XElement refNode = ...
XElement wantedBs = refNode.Descendants("B")
                           .Where(b => parent.Name.LocalName != "B");
Chuck Savage
  • 11,775
  • 6
  • 49
  • 69
  • thanks for your reply. edited my question, the user might mark A as ref node and ask for all C nodes inside it, so this will not work. – Amit Khanna Dec 30 '12 at 06:31