package org.xmlunit.matchers; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Factory; import org.hamcrest.Matcher; import org.w3c.dom.Node; import org.xmlunit.builder.Input; import org.xmlunit.util.Convert; import org.xmlunit.xpath.JAXPXPathEngine; import javax.xml.transform.Source; import java.util.Map; /** * This Hamcrest {@link Matcher} verifies whether the provided XPath expression corresponds to at least * one element in the provided object. * * <p>All types which are supported by {@link Input#from(Object)} can be used as input for the object * against the matcher is evaluated.</p> * * <p><b>Simple Example</b></p> * * <pre> * final String xml = "<a><b attr=\"abc\"></b></a>"; * * assertThat(xml, hasXPath("//a/b/@attr")); * assertThat(xml, not(hasXPath("//a/b/c"))); * </pre> * * <p><b>Example with namespace mapping</b></p> * * <pre> * String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + * "<feed xmlns=\"http://www.w3.org/2005/Atom\">" + * " <title>title</title>" + * " <entry>" + * " <title>title1</title>" + * " <id>id1</id>" + * " </entry>" + * "</feed>"; * * HashMap<String, String> prefix2Uri = new HashMap<String, String>(); * prefix2Uri.put("atom", "http://www.w3.org/2005/Atom"); * assertThat(xmlRootElement, * hasXPath("//atom:feed/atom:entry/atom:id").withNamespaceContext(prefix2Uri)); * </pre> * * @since XMLUnit 2.1.0 */ public class HasXPathMatcher extends BaseMatcher<Object> { private String xPath; private Map<String, String> prefix2Uri; /** * Creates a {@link HasXPathMatcher} instance with the associated XPath expression. * * @param xPath xPath expression */ public HasXPathMatcher(String xPath) { this.xPath = xPath; } @Override public boolean matches(Object object) { JAXPXPathEngine engine = new JAXPXPathEngine(); if (prefix2Uri != null) { engine.setNamespaceContext(prefix2Uri); } Source s = Input.from(object).build(); Node n = Convert.toNode(s); Iterable<Node> nodes = engine.selectNodes(xPath, n); return nodes.iterator().hasNext(); } @Override public void describeTo(Description description) { description.appendText("XML with XPath ").appendText(xPath); } @Override public void describeMismatch(Object item, Description mismatchDescription) { mismatchDescription.appendText("XPath returned no results."); } /** * Creates a matcher that matches when the examined XML input has at least one node * corresponding to the specified <code>xPath</code>. * * <p>For example:</p> * <pre>assertThat(xml, hasXPath("/root/cars[0]/audi"))</pre> * * @param xPath the target xpath * @return the xpath matcher */ @Factory public static HasXPathMatcher hasXPath(String xPath) { return new HasXPathMatcher(xPath); } /** * Utility method used for creating a namespace context mapping to be used in XPath matching. * * @param prefix2Uri prefix2Uri maps from prefix to namespace URI. It is used to resolve * XML namespace prefixes in the XPath expression */ public HasXPathMatcher withNamespaceContext(Map<String, String> prefix2Uri) { this.prefix2Uri = prefix2Uri; return this; } }