tags:

views:

474

answers:

4

I've looked at this question: http://stackoverflow.com/questions/453191/select-first-instance-only-with-xpath

But if I have a node-set like this:


<container value="">
  <data id="1"></data>
  <data id="2">test</data>
  <container>
    <data id="1">test</data>
    <data id="3">test</data>
  </container>
</container>

Now my scenario is that this node-set is deep inside a document, and I have a pointer to the inner container. So I have to prefix my XPath with "/container/container" (the path is actually longer, but for this example this should do).

EDIT: What I want is a "data" node with an id of 1, which should come from the lowest node first or the closest ancestor. So, if I can't find it on the "current" (/container/container) I should look at the ancestors and get the nearest one (or in the end, not find anything). I have tried this:

/container/container/ancestor-or-self::container/data[@id="1"]

Which brings back a result set with two nodes. I thought I could use last() to get the deepest one, so I tacked that on the end, but to no avail. :)

Thanks for any help.

+1  A: 

I'm not 100% clear on what you're asking for, but if I read this right maybe you just want:

/container/container/ancestor-or-self::container[1]/data[@id='1']

note the "[1]"

Edit: thought about it a little more and the below works for me for all cases so far looking for @id=$N

/container/container/ancestor-or-self::container[data[@id=$N]][1]/data[@id=$N]

Pretty extreme xpathing right there. Basically what's happening is that the nodeset from ancestor-or-self returns the lowest order at position 1 - that's the one you want - but you need to put a second condition on it that node also has a data node you're interested in, once you have those conditions met you have the right node now you just want to tunnel further in to the actual data.

99% of the time xpath gets easier if you take it apart and think of it as an algorithm :)

annakata
I tried that, but that doesn't cover every case. What if I'm looking instead for @id='2'? I want to favor the results from the current node before the ancestors, but be able to fallback.
Nick Spacek
See edit: I believe that covers all listed cases
annakata
+1  A: 

How did you try tacking last() on? I think this should work:

/container/container/ancestor-or-self::container/data[@id="1"][last()]

EDIT:

Right, of course, I forgot the parentheses:

(/container/container/ancestor-or-self::container/data[@id="1"])[last()]

Which makes this the same as one of the other answers; however, as the original poster points out, this expression fails on:

<container value="">
  <container>
    <data id="1">a3</data>
    <data id="3">a4</data>
  </container>
  <data id="1">a1</data>
  <data id="2">a2</data>
</container>

True to the spirit of stackoverflow, I can however combine my answer with one of the other answers and get something that works in all cases:

(/container/container/ancestor-or-self::container[data[@id="1"]])[last()]/data[@id="1"]

By the way, if there are multiple @id-is-1 <data> children of one container, this will return all of them. To return only the first such <data> element:

(/container/container/ancestor-or-self::container[data[@id="1"]])[last()]/data[@id="1"][1]
Daniel Martin
If my reading here on the desired result is correct (big if) this won't work because the last() will match on both branches of ancestor-or-self - that's the real problem, so you need to clip the result set either early (as I have) or late (as Dave does).
annakata
This one does it. Thanks a lot (everyone too)!
Nick Spacek
A: 

Instead of

@id="1"

have you tried:

position() == 1
skaffman
If position() == 1 would work then just dropping the ancestor-or-self:: all together. I think the requirement is not for just any first data node but a data node with an id of 1. The Question isn't as clear as it could be.
AnthonyWJones
+1  A: 

Make sure the last() is scoped correctly. Try:

(/container/container/ancestor-or-self::container/data[@id="1"])[last()]

Similarly:

//container[last()]

and:

(//container)[last()]

are not the same thing. The first will return the last container node at each level of the hierarchy - multiple matches - and the second will return the last container node found - one match.

Dave Webb
Aha, this worked! Thank you.
Nick Spacek
Actually, I discovered this doesn't work in the case that the child container node is specified before a matching data element.
Nick Spacek
@Nick Spacek - I improved on my answer below, and it now covers even that corner case
Daniel Martin