package org.tigris.juxy.util; import org.w3c.dom.*; import org.w3c.dom.traversal.DocumentTraversal; import org.w3c.dom.traversal.NodeFilter; import org.w3c.dom.traversal.TreeWalker; import org.xml.sax.SAXException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * @author Pavel Sher */ public class XMLComparator { public static void assertEquals(String expected, String actual) throws DocumentsAssertionError, SAXException { ArgumentAssert.notNull(expected, "Expected document must not be null"); ArgumentAssert.notEmpty(actual, "Actual document must not be empty"); assertEquals(DOMUtil.parse(expected), DOMUtil.parse(actual)); } public static void assertEquals(String expected, Node actual) throws DocumentsAssertionError, SAXException { ArgumentAssert.notNull(expected, "Expected document must not be null"); ArgumentAssert.notNull(actual, "Actual document must not be null"); assertEquals(DOMUtil.parse(expected), actual); } public static void assertEquals(Node expected, Node actual) throws DocumentsAssertionError { ArgumentAssert.notNull(expected, "Expected document must not be null"); ArgumentAssert.notNull(actual, "Actual document must not be null"); Document expectedDoc = expected.getNodeType() == Node.DOCUMENT_NODE ? (Document) expected : expected.getOwnerDocument(); TreeWalker expTw = ((DocumentTraversal) expectedDoc).createTreeWalker(expectedDoc, NodeFilter.SHOW_ALL, new ComparatorNodeFilter(), true); Document actualDoc = actual.getNodeType() == Node.DOCUMENT_NODE ? (Document) actual : actual.getOwnerDocument(); TreeWalker actualTw = ((DocumentTraversal) actualDoc).createTreeWalker(actualDoc, NodeFilter.SHOW_ALL, new ComparatorNodeFilter(), true); Node enode = skipDocumentIfNeeded(expected); Node anode = skipDocumentIfNeeded(actual); if (enode == null && anode == null) return; if (enode == null || anode == null) throw new DocumentsAssertionError(expTw, actualTw); expTw.setCurrentNode(enode); actualTw.setCurrentNode(anode); while (true) { if (enode == null && (anode == null || anode == actual.getNextSibling())) return; if (enode == null || anode == null) throw new DocumentsAssertionError(expTw, actualTw); if (enode.getNodeType() != anode.getNodeType()) throw new DocumentsAssertionError(expTw, actualTw); enode.normalize(); anode.normalize(); switch (enode.getNodeType()) { case Node.TEXT_NODE: case Node.COMMENT_NODE: case Node.CDATA_SECTION_NODE: checkNodeValues(enode, anode, expTw, actualTw); break; case Node.ELEMENT_NODE: checkNodeNames(enode, anode, expTw, actualTw); checkNodeNamespaces(enode, anode, expTw, actualTw); checkAttributes((Element) enode, (Element) anode, expTw, actualTw); break; case Node.PROCESSING_INSTRUCTION_NODE: checkNodeNames(enode, anode, expTw, actualTw); checkNodeValues(enode, anode, expTw, actualTw); break; case Node.DOCUMENT_TYPE_NODE: checkNodeNames(enode, anode, expTw, actualTw); DocumentType edt = (DocumentType) enode; DocumentType adt = (DocumentType) anode; checkStringsEqual(edt.getInternalSubset(), adt.getInternalSubset(), expTw, actualTw); checkStringsEqual(edt.getSystemId(), adt.getSystemId(), expTw, actualTw); checkStringsEqual(edt.getPublicId(), adt.getPublicId(), expTw, actualTw); break; } enode = expTw.nextNode(); anode = actualTw.nextNode(); } } private static Node skipDocumentIfNeeded(Node startFrom) { while (true && startFrom != null) { switch (startFrom.getNodeType()) { case Node.DOCUMENT_NODE: case Node.DOCUMENT_FRAGMENT_NODE: startFrom = startFrom.getFirstChild(); break; case Node.TEXT_NODE: if (startFrom.getNodeValue().trim().length() == 0) { startFrom = startFrom.getNextSibling(); } break; default: return startFrom; } } return startFrom; } private static void checkAttributes(Element enode, Element anode, TreeWalker expTw, TreeWalker actualTw) { NamedNodeMap expattrs = enode.getAttributes(); NamedNodeMap actattrs = anode.getAttributes(); Map efiltered = filterAttributes(expattrs); Map afiltered = filterAttributes(actattrs); if (efiltered.size() != afiltered.size()) throw new DocumentsAssertionError(expTw, actualTw); Iterator attrsIt = efiltered.entrySet().iterator(); while (attrsIt.hasNext()) { Map.Entry aentry = (Map.Entry) attrsIt.next(); String attrName = (String) aentry.getKey(); Node attr = (Node) aentry.getValue(); Node actual = (Node) afiltered.get(attrName); if (actual == null) throw new DocumentsAssertionError(expTw, actualTw); checkNodeNamespaces(attr, actual, expTw, actualTw); checkNodeValues(attr, actual, expTw, actualTw); } } /** * Removes all xmlns attributes * * @return Map */ private static Map filterAttributes(NamedNodeMap attrs) { Map filtered = new HashMap(); for (int i = 0; i < attrs.getLength(); i++) { Node attr = attrs.item(i); if (!attr.getNodeName().startsWith("xmlns")) filtered.put(attr.getNodeName(), attr); } return filtered; } private static void checkNodeNamespaces(Node enode, Node anode, TreeWalker expTw, TreeWalker actualTw) { checkStringsEqual(enode.getNamespaceURI(), anode.getNamespaceURI(), expTw, actualTw); } private static void checkNodeNames(Node enode, Node anode, TreeWalker expTw, TreeWalker actualTw) { String enodeName = enode.getLocalName() == null ? enode.getNodeName() : enode.getLocalName(); String anodeName = anode.getLocalName() == null ? anode.getNodeName() : anode.getLocalName(); if (!enodeName.equals(anodeName)) throw new DocumentsAssertionError(expTw, actualTw); } private static void checkNodeValues(Node enode, Node anode, TreeWalker expTw, TreeWalker actualTw) { String eval = enode.getNodeValue().trim(); String aval = anode.getNodeValue().trim(); if (!eval.equals(aval)) throw new DocumentsAssertionError(expTw, actualTw); } private static void checkStringsEqual(String estr, String astr, TreeWalker expTw, TreeWalker actualTw) { if (estr != null && astr != null) { if (!estr.equals(astr)) throw new DocumentsAssertionError(expTw, actualTw); } else if ((estr != null && estr.length() > 0) || (astr != null && astr.length() > 0)) throw new DocumentsAssertionError(expTw, actualTw); } static class ComparatorNodeFilter implements NodeFilter { public short acceptNode(Node n) { switch (n.getNodeType()) { case Node.DOCUMENT_FRAGMENT_NODE: case Node.ENTITY_NODE: case Node.ENTITY_REFERENCE_NODE: case Node.NOTATION_NODE: // parsed entities will be automatically expanded, // notations are not supported return NodeFilter.FILTER_SKIP; case Node.TEXT_NODE: if (n.getNodeValue().trim().length() == 0) return NodeFilter.FILTER_SKIP; break; } return NodeFilter.FILTER_ACCEPT; } } }