/* (c) 2015 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.util; import static org.hamcrest.Matchers.any; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasItem; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.AbstractList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.custommonkey.xmlunit.SimpleNamespaceContext; import org.custommonkey.xmlunit.XMLUnit; import org.custommonkey.xmlunit.XpathEngine; import org.custommonkey.xmlunit.exceptions.XpathException; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * An aid for XML related testing. * * @author Kevin Smith, Boundless * */ public class XmlTestUtil { private OutputStream showXML = null; private Map<String, String> namespaces = new HashMap<>(); private org.custommonkey.xmlunit.NamespaceContext namespaceContext; private void regenerateContext() { namespaceContext=new SimpleNamespaceContext(namespaces); } public XmlTestUtil() { regenerateContext(); } /** * Set an output stream to print XML to when a matcher fails. Null to disable. * @param showXML */ public void setShowXML(OutputStream showXML) { this.showXML = showXML; } /** * Add a namespace to be used when resolving XPath expressions. * @param prefix * @param uri */ public void addNamespace(String prefix, String uri) { namespaces.put(prefix, uri); regenerateContext(); } /** * Match a document where one node matched the XPath expression, and it also matches the given matcher. * @param xPath * @param matcher * */ @SuppressWarnings({ "unchecked", "rawtypes" }) public Matcher<Document> hasOneNode(final String xPath, final Matcher<? super Node> matcher) { return hasNodes(xPath, (Matcher)contains(matcher)); } /** * Match a document where one node matches the XPath expression. * @param xPath * @param matcher * */ public Matcher<Document> hasOneNode(final String xPath) { return hasOneNode(xPath, any(Node.class)); } /** * Match a document at least one of the nodes matched by the given XPath expression matches the given matcher. * @param xPath * @param matcher * */ @SuppressWarnings({ "unchecked", "rawtypes" }) public Matcher<Document> hasNode(final String xPath, final Matcher<Node> matcher) { return hasNodes(xPath, (Matcher) hasItem(matcher)); } /** * Match a document at least one node matches the given XPath. * @param xPath * @param matcher * */ public Matcher<Document> hasNode(final String xPath) { return hasNode(xPath, any(Node.class)); } /** * Match a document where the list of nodes selected by the given XPath expression also matches the given matcher. * @param xPath * @param matcher * */ public Matcher<Document> hasNodes(final String xPath, final Matcher<? extends Iterable<Node>> matcher) { return new BaseMatcher<Document>() { @Override public boolean matches(Object item) { XpathEngine engine = XMLUnit.newXpathEngine(); engine.setNamespaceContext(namespaceContext); try { List<Node> nodes = nodeCollection(engine.getMatchingNodes(xPath, (Document) item)); return matcher.matches(nodes); } catch (XpathException e) { return false; } } @Override public void describeTo(Description description) { description.appendText("Document where the list of nodes matching ") .appendValue(xPath).appendText(" is ").appendDescriptionOf(matcher); } @Override public void describeMismatch(Object item, Description description) { XpathEngine engine = XMLUnit.newXpathEngine(); engine.setNamespaceContext(namespaceContext); try { List<Node> nodes = nodeCollection(engine.getMatchingNodes(xPath, (Document) item)); matcher.describeMismatch(nodes, description); if(showXML != null) { printDom((Document) item, showXML); } } catch (XpathException e) { description.appendText("exception occured: ").appendText(e.getMessage()); } } }; } /** * Make a Java List out of a DOM NodeList. * @param nl * */ public static List<Node> nodeCollection(final NodeList nl) { return new AbstractList<Node>() { @Override public Node get(int index) { return nl.item(index); } @Override public int size() { return nl.getLength(); } }; } /** * Print a DOM tree to an output stream or if there is an exception while doing so, print the stack trace. * @param dom * @param os */ public static void printDom(Node dom, OutputStream os) { Transformer trans; PrintWriter w = new PrintWriter(os); try { TransformerFactory fact = TransformerFactory.newInstance(); trans = fact.newTransformer(); trans.transform(new DOMSource(dom), new StreamResult(new OutputStreamWriter(os))); } catch (TransformerException e) { w.println("An error ocurred while transforming the given DOM:"); e.printStackTrace(w); } } }