/*
* Copyright 2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xmlmatchers.xpath;
import static org.xmlmatchers.xpath.XpathReturnType.returningAString;
import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFactoryConfigurationException;
import net.sf.saxon.lib.NamespaceConstant;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.w3c.dom.Node;
import org.xmlmatchers.transform.IdentityTransformer;
import org.xmlmatchers.transform.StringResult;
/**
*
*
* @author David Ehringer
* @param <T>
* The type to execute the XPath against
*/
public class HasXPath extends TypeSafeMatcher<Source> {
// TODO change to extending TypeSafeDiagnosingMatcher
private static final IdentityTransformer IDENTITY = new IdentityTransformer();
private final String xPathString;
private final XPathExpression compiledXPath;
private final XpathReturnType<?> xPathReturnType;
private final Matcher<?> valueMatcher;
protected HasXPath(String xPathExpression) {
this(xPathExpression, null, null);
}
protected HasXPath(String xPathExpression, Matcher<? super String> valueMatcher,
NamespaceContext namespaceContext) {
this(xPathExpression, valueMatcher, namespaceContext, returningAString());
}
// This works when using the Eclipse compiler but now with with Sun's. Not sure why
// public <R> HasXPath(String xPathExpression, Matcher<? super R> valueMatcher,
// NamespaceContext namespaceContext, XpathReturnType<? super R> xPathReturnType) {
public HasXPath(String xPathExpression, Matcher<?> valueMatcher,
NamespaceContext namespaceContext, XpathReturnType<?> xPathReturnType) {
try {
XPath xPath = buildXPath();
if (namespaceContext != null) {
xPath.setNamespaceContext(namespaceContext);
}
compiledXPath = xPath.compile(xPathExpression);
this.xPathString = xPathExpression;
this.valueMatcher = valueMatcher;
} catch (XPathExpressionException e) {
throw new IllegalArgumentException("Invalid XPath : "
+ xPathExpression, e);
}
if (xPathReturnType == null) {
this.xPathReturnType = returningAString();
} else {
this.xPathReturnType = xPathReturnType;
}
}
private XPath buildXPath() {
System.setProperty("javax.xml.xpath.XPathFactory:"
+ NamespaceConstant.OBJECT_MODEL_SAXON,
"net.sf.saxon.xpath.XPathFactoryImpl");
try {
return XPathFactory.newInstance(NamespaceConstant.OBJECT_MODEL_SAXON).newXPath();
} catch (XPathFactoryConfigurationException e) {
throw new UnsupportedOperationException(
"Saxon is incorrectly configured or not available on the classpath",
e);
}
}
public void describeTo(Description description) {
description.appendText("an XML document with XPath " + xPathString);
if (valueMatcher != null) {
description.appendText(" matching ");
valueMatcher.describeTo(description);
}
}
@Override
public boolean matchesSafely(Source source) {
try {
Node node = convert(source);
if (valueMatcher == null) {
return evaluateXPathForExistence(node);
}
Object xPathResult = evaluateXPath(node);
return valueMatcher.matches(xPathResult);
} catch (XPathExpressionException e) {
return false;
} catch (TransformerException e) {
return false;
} catch (IllegalArgumentException e) {
return false;
}
}
private Node convert(Source source) {
DOMResult dom = new DOMResult();
IDENTITY.transform(source, dom);
return dom.getNode();
}
private boolean evaluateXPathForExistence(Node node)
throws TransformerException, XPathExpressionException {
return compiledXPath.evaluate(node, XPathConstants.NODE) == null ? false
: true;
}
/**
* @see {@link XpathReturnType#returningAnXmlNode()} about implementation details.
*/
private Object evaluateXPath(Node node) throws TransformerException,
XPathExpressionException {
if (XPathConstants.NODE == xPathReturnType.evaluationMode()) {
// We need a special case for XML results so that we actually get back the XML in a format useful for chaining other matchers
Node result = (Node) compiledXPath.evaluate(node,
XPathConstants.NODE);
return new DOMSource(result);
}
return compiledXPath.evaluate(node, xPathReturnType.evaluationMode());
}
@Factory
public static Matcher<Source> hasXPath(String xPath) {
return new HasXPath(xPath);
}
@Factory
public static Matcher<Source> hasXPath(String xPath,
NamespaceContext namespaceContext) {
return new HasXPath(xPath, null, namespaceContext);
}
@Factory
public static Matcher<Source> hasXPath(String xPath,
Matcher<? super String> valueMatcher) {
return new HasXPath(xPath, valueMatcher, null);
}
@Factory
public static Matcher<Source> hasXPath(String xPath,
NamespaceContext namespaceContext, Matcher<? super String> valueMatcher) {
return new HasXPath(xPath, valueMatcher, namespaceContext);
}
@Factory
public static <T> Matcher<Source> hasXPath(String xPath,
XpathReturnType<? super T> xpathReturnType,
Matcher<? super T> valueMatcher) {
return new HasXPath(xPath, valueMatcher, null, xpathReturnType);
}
@Factory
public static <T> Matcher<Source> hasXPath(String xPath,
NamespaceContext namespaceContext,
XpathReturnType<? super T> xpathReturnType,
Matcher<? super T> valueMatcher) {
return new HasXPath(xPath, valueMatcher, namespaceContext,
xpathReturnType);
}
@Factory
public static Matcher<Source> hasXPath(String xPath,
Matcher<? super String> valueMatcher, NamespaceContext namespaceContext) {
return new HasXPath(xPath, valueMatcher, namespaceContext);
}
}