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 evaluation of the provided XPath expression
* corresponds to the value matcher specified for the provided input XML object.
*
* <p>All types which are supported by {@link Input#from(Object)} can be used as input for the XML 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", equalTo("abc")));
* assertThat(xml, hasXPath("count(//a/b/c)", equalTo("0")));
* </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(xml,
* hasXPath("//atom:feed/atom:entry/atom:id/text()", equalTo("id1"))
* .withNamespaceContext(prefix2Uri));
* </pre>
*
* @since XMLUnit 2.1.0
*/
public class EvaluateXPathMatcher extends BaseMatcher<Object> {
private final String xPath;
private final Matcher<String> valueMatcher;
private Map<String, String> prefix2Uri;
/**
* Creates a {@link EvaluateXPathMatcher} instance with the associated XPath expression and
* the value matcher corresponding to the XPath evaluation.
*
* @param xPath xPath expression
* @param valueMatcher matcher for the value at the specified xpath
*/
public EvaluateXPathMatcher(String xPath, Matcher<String> valueMatcher) {
this.xPath = xPath;
this.valueMatcher = valueMatcher;
}
/**
* Creates a matcher that matches when the examined XML input has a value at the
* specified <code>xPath</code> that satisfies the specified <code>valueMatcher</code>.
*
* <p>For example:</p>
* <pre>assertThat(xml, hasXPath("//fruits/fruit/@name", equalTo("apple"))</pre>
*
* @param xPath the target xpath
* @param valueMatcher matcher for the value at the specified xpath
* @return the xpath matcher
*/
@Factory
public static EvaluateXPathMatcher hasXPath(String xPath, Matcher<String> valueMatcher) {
return new EvaluateXPathMatcher(xPath, valueMatcher);
}
@Override
public boolean matches(Object object) {
String value = xPathEvaluate(object);
return valueMatcher.matches(value);
}
@Override
public void describeTo(Description description) {
description.appendText("XML with XPath ").appendText(xPath);
if (valueMatcher != null) {
description.appendText(" evaluated to ").appendDescriptionOf(valueMatcher);
}
}
@Override
public void describeMismatch(Object object, Description mismatchDescription) {
if (valueMatcher != null) {
String value = xPathEvaluate(object);
valueMatcher.describeMismatch(value, mismatchDescription);
}
}
/**
* 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 EvaluateXPathMatcher withNamespaceContext(Map<String, String> prefix2Uri) {
this.prefix2Uri = prefix2Uri;
return this;
}
/**
* Evaluates the provided XML input to the configured <code>xPath</code> field XPath expression.
* @param input an XML input
* @return the result of the XPath evaluation
*/
private String xPathEvaluate(Object input) {
JAXPXPathEngine engine = new JAXPXPathEngine();
if (prefix2Uri != null) {
engine.setNamespaceContext(prefix2Uri);
}
Source s = Input.from(input).build();
Node n = Convert.toNode(s);
return engine.evaluate(xPath, n);
}
}