XPath Descendant Attribute Filter With Contains

Viewed 405

Using the XML below, the XPath without contains matches 1 element, but one with contains matches 0 elements.

//pet[@type = "dog"][.//@name = "Acme Dog Food"]
//pet[@type = "dog"][contains(.//@name, "Acme Dog Food")]

I would expect both to match the pet[@name = "Fido"] element. Why is there a difference?

<documentRoot>
<!-- Test data -->
<?value="2"?>
<parent name="data" >
   <child id="1"  name="alpha" >Some Text</child>
   <child id="2"  name="beta" >
      <grandchild id="2.1"  name="beta-alpha" ></grandchild>
      <grandchild id="2.2"  name="beta-beta" ></grandchild>
   </child>
   <pet name="tigger"  type="cat" >
      <data>
         <birthday month="sept"  day="19" ></birthday>
         <food name="Acme Cat Food" ></food>
      </data>
   </pet>
   <pet name="Fido"  type="dog" >
      <description>
         Large dog!
      </description>
      <data>
         <birthday month="feb"  day="3" ></birthday>
         <food name="Acme Dog Food" ></food>
      </data>
   </pet>
   <rogue name="is this real?" >
      <data>
         Hates dogs!
      </data>
   </rogue>
   <child id="3"  name="gamma"  mark="yes" >
      <!-- A comment -->
      <description>
         Likes all animals - especially dogs!
      </description>
      <grandchild id="3.1"  name="gamma-alpha" >
      </grandchild>
      <grandchild id="3.2"  name="gamma-beta" ></grandchild>
   </child>
</parent>
</documentRoot>

Online XPath Tester: http://www.whitebeam.org/library/guide/TechNotes/xpathtestbed.rhtm

2 Answers

Explanation

XPath 1.0

When exp in contains(exp, substring) evaluates to a nodeset, the first node's string value is used as the first argument to contains(). In your case, the first node of that nodeset is not the attribute in this element,

<food name="Acme Dog Food" ></food>

but rather that name attribute in this element

<pet name="Fido"  type="dog" >

which clearly contains no "Acme Dog Food" substring.

XPath 2.0

When exp in contains(exp, substring) evaluates to a sequence of items, an error results.

Solutions

You can avoid the problem of matching multiple nodes in the first argument of contains() by testing the string value of each attribute individually (as already posted by @DanielHaley, please upvote his answer):

//pet[@type = "dog"][.//@name[contains(., "Acme Dog Food")]]

You can avoid testing the current node by replacing the .// abbreviation for the descendant-or-self:: axis with just the descendant:: axis:

//pet[@type = "dog"][descendant::*/@name[contains(., "Acme Dog Food")]]

Finally, if you don't really need substring checking – if you don't want to match "Smelly Acme Dog Food" – use an equality test rather than contains():

//pet[@type = "dog"][.//@name = "Acme Dog Food"]

as you had in your original XPath. ;-)

Since you're using XPath 1.0, the contains(.//@name, "Acme Dog Food") is only evaluating the first @name it finds.

To resolve this, put the "contains" predicate on the attribute:

//pet[@type = "dog"][.//@name[contains(., "Acme Dog Food")]]

or specify the food element:

//pet[@type = "dog"][contains(.//food/@name, "Acme Dog Food")]
Related