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