package com.rcpcompany.test.utils.xml; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.Text; import org.xml.sax.SAXException; /** * Utility class for finding differences in XML structures * * @author Tonny Madsen, tonny.madsen@gmail.com */ public class XMLDiff { private final boolean nodeTypeDiff = true; private final boolean nodeValueDiff = true; private static DocumentBuilderFactory theFactory; public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { if (theFactory == null) { theFactory = DocumentBuilderFactory.newInstance(); // dbf.setNamespaceAware(true); // dbf.setValidating(true); theFactory.setCoalescing(false); theFactory.setIgnoringElementContentWhitespace(true); theFactory.setIgnoringComments(true); } return theFactory.newDocumentBuilder(); } /** * Converts a String with XML to the corresponding XML Node. * * @param xml * the XML string * @return the XML Node * @throws ParserConfigurationException * @throws SAXException * @throws IOException */ public static Node toNode(String xml) throws ParserConfigurationException, SAXException, IOException { return toNode(new ByteArrayInputStream(xml.getBytes("UTF-8"))); } public static Node toNode(InputStream is) throws ParserConfigurationException, SAXException, IOException { final DocumentBuilder db = newDocumentBuilder(); final Document doc = db.parse(is); doc.normalizeDocument(); return doc; } /** * Returns a description of the differences between the two XML strings. * * @param xml1 * XML String #1 * @param xml2 * XML String #2 * @param diffs * the differences * @return <code>true</code> if there are any differences * @throws Exception */ public boolean diff(String xml1, String xml2, List<String> diffs) throws Exception { return diff(toNode(xml1), toNode(xml2), diffs); } /** * Returns a description of the differences between the two XML Nodes. * * @param node1 * XML Node #1 * @param node2 * XML Node #2 * @param diffs * the differences * @return <code>true</code> if there are any differences * @throws Exception */ public boolean diff(Node node1, Node node2, List<String> diffs) throws Exception { if (diffNodeExists(node1, node2, diffs)) return true; if (nodeTypeDiff) { diffNodeType(node1, node2, diffs); } if (nodeValueDiff) { diffNodeValue(node1, node2, diffs); } // System.out.println(node1.getNodeName() + "/" + node2.getNodeName()); diffAttributes(node1, node2, diffs); diffNodes(node1, node2, diffs); return diffs.size() > 0; } /** * Diff the nodes */ private boolean diffNodes(Node node1, Node node2, List<String> diffs) throws Exception { // Sort by Name final Map<String, List<Node>> children1 = new HashMap<String, List<Node>>(); for (Node child1 = node1.getFirstChild(); child1 != null; child1 = child1.getNextSibling()) { if (child1 instanceof Text && child1.getNodeValue().trim().isEmpty()) { continue; } List<Node> list = children1.get(child1.getNodeName()); if (list == null) { list = new ArrayList<Node>(); children1.put(child1.getNodeName(), list); } list.add(child1); } // Sort by Name final Map<String, List<Node>> children2 = new HashMap<String, List<Node>>(); for (Node child2 = node2.getFirstChild(); child2 != null; child2 = child2.getNextSibling()) { if (child2 instanceof Text && child2.getNodeValue().trim().isEmpty()) { continue; } List<Node> list = children2.get(child2.getNodeName()); if (list == null) { list = new ArrayList<Node>(); children2.put(child2.getNodeName(), list); } list.add(child2); } // Diff all the children1 for (final List<Node> list1 : children1.values()) { for (final Node child1 : list1) { final List<Node> list2 = children2.get(child1.getNodeName()); Node child2 = null; if (list2 != null) { final Iterator<Node> c2iterator = list2.iterator(); if (c2iterator.hasNext()) { child2 = c2iterator.next(); c2iterator.remove(); } } diff(child1, child2, diffs); } } // Diff all the children2 left over for (final List<Node> list2 : children2.values()) { for (final Node child2 : list2) { diff(null, child2, diffs); } } return diffs.size() > 0; } /** * Diff the nodes */ private boolean diffAttributes(Node node1, Node node2, List<String> diffs) throws Exception { // Sort by Name final NamedNodeMap nodeMap1 = node1.getAttributes(); final Map<String, Node> attributes1 = new LinkedHashMap<String, Node>(); for (int index = 0; nodeMap1 != null && index < nodeMap1.getLength(); index++) { attributes1.put(nodeMap1.item(index).getNodeName(), nodeMap1.item(index)); } // Sort by Name final NamedNodeMap nodeMap2 = node2.getAttributes(); final Map<String, Node> attributes2 = new LinkedHashMap<String, Node>(); for (int index = 0; nodeMap2 != null && index < nodeMap2.getLength(); index++) { attributes2.put(nodeMap2.item(index).getNodeName(), nodeMap2.item(index)); } // Diff all the attributes1 for (final Node attribute1 : attributes1.values()) { final Node attribute2 = attributes2.remove(attribute1.getNodeName()); diff(attribute1, attribute2, diffs); } // Diff all the attributes2 left over for (final Node attribute2 : attributes2.values()) { final Node attribute1 = attributes1.get(attribute2.getNodeName()); diff(attribute1, attribute2, diffs); } return diffs.size() > 0; } /** * Check that the nodes exist */ private boolean diffNodeExists(Node node1, Node node2, List<String> diffs) throws Exception { if (node1 == null && node2 == null) { diffs.add(getPath(node2) + ":node " + node1 + "!=" + node2 + "\n"); return true; } if (node1 == null && node2 != null) { diffs.add(getPath(node2) + ":node " + node1 + "!=" + node2.getNodeName()); return true; } if (node1 != null && node2 == null) { diffs.add(getPath(node1) + ":node " + node1.getNodeName() + "!=" + node2); return true; } return false; } /** * Diff the Node Type */ private boolean diffNodeType(Node node1, Node node2, List<String> diffs) throws Exception { if (node1.getNodeType() != node2.getNodeType()) { diffs.add(getPath(node1) + ":type " + node1.getNodeType() + "!=" + node2.getNodeType()); return true; } return false; } /** * Diff the Node Value */ private boolean diffNodeValue(Node node1, Node node2, List<String> diffs) throws Exception { if (node1.getNodeValue() == null && node2.getNodeValue() == null) return false; if (node1.getNodeValue() == null && node2.getNodeValue() != null) { diffs.add(getPath(node1) + ":type " + node1 + "!=" + node2.getNodeValue()); return true; } if (node1.getNodeValue() != null && node2.getNodeValue() == null) { diffs.add(getPath(node1) + ":type " + node1.getNodeValue() + "!=" + node2); return true; } if (!node1.getNodeValue().equals(node2.getNodeValue())) { diffs.add(getPath(node1) + ":type " + node1.getNodeValue() + "!=" + node2.getNodeValue()); return true; } return false; } /** * Get the node path * * @param node * an XML node * @return the XPath to the node */ public String getPath(Node node) { final StringBuilder path = new StringBuilder(); do { path.insert(0, node.getNodeName()); path.insert(0, "/"); } while ((node = node.getParentNode()) != null); return path.toString(); } }