• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.hamcrest.xml;
2 
3 import org.hamcrest.Condition;
4 import org.hamcrest.Description;
5 import org.hamcrest.Matcher;
6 import org.hamcrest.TypeSafeDiagnosingMatcher;
7 import org.hamcrest.core.IsAnything;
8 import org.w3c.dom.Node;
9 
10 import javax.xml.namespace.NamespaceContext;
11 import javax.xml.namespace.QName;
12 import javax.xml.xpath.*;
13 
14 import static javax.xml.xpath.XPathConstants.STRING;
15 import static org.hamcrest.Condition.matched;
16 import static org.hamcrest.Condition.notMatched;
17 
18 /**
19  * Applies a Matcher to a given XML Node in an existing XML Node tree, specified by an XPath expression.
20  *
21  * @author Joe Walnes
22  * @author Steve Freeman
23  */
24 public class HasXPath extends TypeSafeDiagnosingMatcher<Node> {
25     public static final NamespaceContext NO_NAMESPACE_CONTEXT = null;
26     private static final IsAnything<String> WITH_ANY_CONTENT = new IsAnything<String>("");
27     private static final Condition.Step<Object,String> NODE_EXISTS = nodeExists();
28     private final Matcher<String> valueMatcher;
29     private final XPathExpression compiledXPath;
30     private final String xpathString;
31     private final QName evaluationMode;
32 
33     /**
34      * @param xPathExpression XPath expression.
35      * @param valueMatcher Matcher to use at given XPath.
36      *                     May be null to specify that the XPath must exist but the value is irrelevant.
37      */
HasXPath(String xPathExpression, Matcher<String> valueMatcher)38     public HasXPath(String xPathExpression, Matcher<String> valueMatcher) {
39         this(xPathExpression, NO_NAMESPACE_CONTEXT, valueMatcher);
40     }
41 
42     /**
43      * @param xPathExpression XPath expression.
44      * @param namespaceContext Resolves XML namespace prefixes in the XPath expression
45      * @param valueMatcher Matcher to use at given XPath.
46      *                     May be null to specify that the XPath must exist but the value is irrelevant.
47      */
HasXPath(String xPathExpression, NamespaceContext namespaceContext, Matcher<String> valueMatcher)48     public HasXPath(String xPathExpression, NamespaceContext namespaceContext, Matcher<String> valueMatcher) {
49         this(xPathExpression, namespaceContext, valueMatcher, STRING);
50     }
51 
HasXPath(String xPathExpression, NamespaceContext namespaceContext, Matcher<String> valueMatcher, QName mode)52     private HasXPath(String xPathExpression, NamespaceContext namespaceContext, Matcher<String> valueMatcher, QName mode) {
53         this.compiledXPath = compiledXPath(xPathExpression, namespaceContext);
54         this.xpathString = xPathExpression;
55         this.valueMatcher = valueMatcher;
56         this.evaluationMode = mode;
57     }
58 
59     @Override
matchesSafely(Node item, Description mismatch)60     public boolean matchesSafely(Node item, Description mismatch) {
61         return evaluated(item, mismatch)
62                .and(NODE_EXISTS)
63                .matching(valueMatcher);
64     }
65 
66     @Override
describeTo(Description description)67     public void describeTo(Description description) {
68         description.appendText("an XML document with XPath ").appendText(xpathString);
69         if (valueMatcher != null) {
70             description.appendText(" ").appendDescriptionOf(valueMatcher);
71         }
72     }
73 
evaluated(Node item, Description mismatch)74     private Condition<Object> evaluated(Node item, Description mismatch) {
75         try {
76             return matched(compiledXPath.evaluate(item, evaluationMode), mismatch);
77         } catch (XPathExpressionException e) {
78             mismatch.appendText(e.getMessage());
79         }
80         return notMatched();
81     }
82 
nodeExists()83     private static Condition.Step<Object, String> nodeExists() {
84         return new Condition.Step<Object, String>() {
85             @Override
86             public Condition<String> apply(Object value, Description mismatch) {
87                 if (value == null) {
88                     mismatch.appendText("xpath returned no results.");
89                     return notMatched();
90                 }
91                 return matched(String.valueOf(value), mismatch);
92             }
93         };
94     }
95 
96     private static XPathExpression compiledXPath(String xPathExpression, NamespaceContext namespaceContext) {
97         try {
98             final XPath xPath = XPathFactory.newInstance().newXPath();
99             if (namespaceContext != null) {
100                 xPath.setNamespaceContext(namespaceContext);
101             }
102             return xPath.compile(xPathExpression);
103         } catch (XPathExpressionException e) {
104             throw new IllegalArgumentException("Invalid XPath : " + xPathExpression, e);
105         }
106     }
107 
108 
109     /**
110      * Creates a matcher of {@link org.w3c.dom.Node}s that matches when the examined node has a value at the
111      * specified <code>xPath</code> that satisfies the specified <code>valueMatcher</code>.
112      * For example:
113      * <pre>assertThat(xml, hasXPath("/root/something[2]/cheese", equalTo("Cheddar")))</pre>
114      *
115      * @param xPath
116      *     the target xpath
117      * @param valueMatcher
118      *     matcher for the value at the specified xpath
119      */
120     public static Matcher<Node> hasXPath(String xPath, Matcher<String> valueMatcher) {
121         return hasXPath(xPath, NO_NAMESPACE_CONTEXT, valueMatcher);
122     }
123 
124     /**
125      * Creates a matcher of {@link org.w3c.dom.Node}s that matches when the examined node has a value at the
126      * specified <code>xPath</code>, within the specified <code>namespaceContext</code>, that satisfies
127      * the specified <code>valueMatcher</code>.
128      * For example:
129      * <pre>assertThat(xml, hasXPath("/root/something[2]/cheese", myNs, equalTo("Cheddar")))</pre>
130      *
131      * @param xPath
132      *     the target xpath
133      * @param namespaceContext
134      *     the namespace for matching nodes
135      * @param valueMatcher
136      *     matcher for the value at the specified xpath
137      */
138     public static Matcher<Node> hasXPath(String xPath, NamespaceContext namespaceContext, Matcher<String> valueMatcher) {
139         return new HasXPath(xPath, namespaceContext, valueMatcher, STRING);
140     }
141 
142     /**
143      * Creates a matcher of {@link org.w3c.dom.Node}s that matches when the examined node contains a node
144      * at the specified <code>xPath</code>, with any content.
145      * For example:
146      * <pre>assertThat(xml, hasXPath("/root/something[2]/cheese"))</pre>
147      *
148      * @param xPath
149      *     the target xpath
150      */
151     public static Matcher<Node> hasXPath(String xPath) {
152         return hasXPath(xPath, NO_NAMESPACE_CONTEXT);
153     }
154 
155     /**
156      * Creates a matcher of {@link org.w3c.dom.Node}s that matches when the examined node contains a node
157      * at the specified <code>xPath</code> within the specified namespace context, with any content.
158      * For example:
159      * <pre>assertThat(xml, hasXPath("/root/something[2]/cheese", myNs))</pre>
160      *
161      * @param xPath
162      *     the target xpath
163      * @param namespaceContext
164      *     the namespace for matching nodes
165      */
166     public static Matcher<Node> hasXPath(String xPath, NamespaceContext namespaceContext) {
167         return new HasXPath(xPath, namespaceContext, WITH_ANY_CONTENT, XPathConstants.NODE);
168     }
169 }
170