1 /* 2 Copyright (C) 2000 - 2007 Grid Systems, S.A. 3 4 This program is free software; you can redistribute it and/or modify 5 it under the terms of the GNU General Public License, version 2, as 6 published by the Free Software Foundation. 7 8 This program is distributed in the hope that it will be useful, 9 but WITHOUT ANY WARRANTY; without even the implied warranty of 10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License 14 along with this program; if not, write to the Free Software 15 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 16 */ 17 package com.gridsystems; 18 19 import java.io.File; 20 import java.io.IOException; 21 import java.io.Reader; 22 import java.util.ArrayList; 23 import java.util.Collection; 24 import java.util.Iterator; 25 26 import javax.xml.parsers.ParserConfigurationException; 27 28 import org.apache.commons.logging.Log; 29 import org.apache.commons.logging.LogFactory; 30 import org.w3c.dom.Document; 31 import org.w3c.dom.Element; 32 import org.w3c.dom.NamedNodeMap; 33 import org.w3c.dom.Node; 34 import org.w3c.dom.NodeList; 35 import org.xml.sax.SAXException; 36 37 /** 38 * "Logical" comparator for XML trees. 39 * <p> 40 * This class implements a "relaxed" comparison between two XML documents. 41 * Starting from the document root elements, a recursive comparison is performed 42 * for each pair of nodes, using the following criteria to determine their 43 * equivalency: 44 * 45 * <ol> 46 * <li>XML comment clauses are ignored.</li> 47 * <li>Text nodes among nested elements are ignored.</li> 48 * <li>Both elements must have the same name, and same attribute set 49 * (the attribute order is not important).</li> 50 * <li>If both elements contain a single child of type TEXT, the contents 51 * must be the same for both nodes.</li> 52 * <li>Both elements must have the same number of child elements</li> 53 * <li>For each child element in the "left" element, an equivalent element 54 * must be present in the "right" one. This equivalence is determined 55 * through a recursive call to the same comparison algorithm. The 56 * order of children elements is not taken into account.</li> 57 * </ol> 58 * 59 * <b>NOTE</b>: Ignoring text nodes among elements allows the following two XML 60 * fragments to be considered as equivalent: 61 * 62 * <pre> 63 * <field> 64 * <value>123</value> 65 * </field> 66 * 67 * <field><value>123</value><field> 68 * </pre> 69 * 70 * @author Job Torres 71 * @author Rodrigo Ruiz 72 * @version 2.0 73 */ 74 public class GridXMLComparator { 75 76 /** 77 * Class logger. 78 */ 79 private static final Log LOG = LogFactory.getLog(GridXMLComparator.class); 80 81 /** 82 * Creates an instance. 83 */ 84 public GridXMLComparator() { 85 } 86 87 /** 88 * Compares two XML documents for equivalency. 89 * 90 * @param xml1 A reader containing the first document to compare 91 * @param xml2 A reader containing the second document to compare 92 * @return <tt>true</tt> if both documents are equivalent; 93 * <tt>false</tt> otherwise 94 * @throws SAXException If a syntax error is found in one of the XML trees 95 * @throws IOException If an I/O error occurs reading the sources 96 * @throws ParserConfigurationException If the builder cannot be instantiated 97 */ 98 public boolean compare(Reader xml1, Reader xml2) 99 throws SAXException, IOException, ParserConfigurationException { 100 101 Document doc1 = XmlUtils.parse(xml1); 102 Document doc2 = XmlUtils.parse(xml2); 103 104 return compare(doc1, doc2); 105 } 106 107 /** 108 * Compares two XML documents for equivalency. 109 * 110 * @param f1 A file containing the first document to compare 111 * @param f2 A file containing the second document to compare 112 * @return <tt>true</tt> if both documents are equivalent; 113 * <tt>false</tt> otherwise 114 * @throws SAXException If a syntax error is found in one of the XML trees 115 * @throws IOException If an I/O error occurs reading the sources 116 * @throws ParserConfigurationException If the builder cannot be instantiated 117 */ 118 public boolean compare(File f1, File f2) 119 throws SAXException, IOException, ParserConfigurationException { 120 121 Document doc1 = XmlUtils.parse(f1); 122 Document doc2 = XmlUtils.parse(f2); 123 124 return compare(doc1, doc2); 125 } 126 127 /** 128 * Compares the given documents. 129 * 130 * @param d1 The first document 131 * @param d2 The second document 132 * @return <tt>true</tt> if they are "logically" equal 133 */ 134 public boolean compare(Document d1, Document d2) { 135 Element xml1Root = d1.getDocumentElement(); 136 Element xml2Root = d2.getDocumentElement(); 137 return compare(xml1Root, xml2Root); 138 } 139 140 /** 141 * Compares the given elements. 142 * 143 * @param elem1 Left element to compare 144 * @param elem2 Right element to compare 145 * @return <tt>true</tt> if both elements are equivalent; 146 * <tt>false</tt> otherwise 147 */ 148 public boolean compare(Element elem1, Element elem2) { 149 // Element names must be the same 150 String name1 = elem1.getNodeName(); 151 String name2 = elem2.getNodeName(); 152 if (!name1.equals(name2)) { 153 LOG.debug("Element names are different ('" + name1 + "', '" + name2 + "')"); 154 return false; 155 } 156 157 // Element attributes must be the same 158 if (!compare(elem1.getAttributes(), elem2.getAttributes())) { 159 return false; 160 } 161 162 return compareChildren(elem1, elem2); 163 /* 164 // Compares element children 165 NodeList children1 = node1.getChildNodes(); 166 NodeList children2 = node2.getChildNodes(); 167 168 final int len1 = children1.getLength(); 169 final int len2 = children2.getLength(); 170 171 if (len1 == 0 && len2 == 0) { 172 // No children, so the elements are equal 173 return true; 174 } else if (1 == len1 && 1 == len2 175 && children1.item(0).getNodeType() == Node.TEXT_NODE 176 && children2.item(0).getNodeType() == Node.TEXT_NODE) { 177 // Only one child of type text: compare text contents 178 String text1 = children1.item(0).getNodeValue(); 179 String text2 = children2.item(0).getNodeValue(); 180 boolean result = text1.equals(text2); 181 if (!result) { 182 log.debug("Text contents differences found"); 183 } 184 return result; 185 } else { 186 // Restrict the comparison to child 'Elements' 187 Collection<Element> elems1 = getElements(children1); 188 Collection<Element> elems2 = getElements(children2); 189 190 if (elems1.size() == elems2.size()) { 191 192 // Look for a correspondence between elements in n1, and any 193 // element in n2 194 Iterator<Element> it1 = elems1.iterator(); 195 loop: 196 while (it1.hasNext()) { 197 Element e1 = it1.next(); 198 199 Iterator<Element> it2 = elems2.iterator(); 200 while (it2.hasNext()) { 201 Element e2 = it2.next(); 202 203 if (compare(e1, e2)) { 204 it2.remove(); 205 continue loop; 206 } 207 } 208 // Correspondence not found. Nodes are different! 209 log.debug("Correspondence not found for child element"); 210 return false; 211 } 212 return true; 213 } else { 214 log.debug("Elements have different number of children"); 215 return false; 216 } 217 }*/ 218 } 219 220 /** 221 * Compares the children nodes of the specified elements for equivalency. 222 * 223 * @param e1 The left element to compare 224 * @param e2 The right element to compare 225 * @return true if they are equivalent 226 */ 227 private boolean compareChildren(Element e1, Element e2) { 228 String name1 = e1.getNodeName(); 229 String name2 = e2.getNodeName(); 230 231 NodeList list1 = e1.getChildNodes(); 232 NodeList list2 = e2.getChildNodes(); 233 234 if (list1.getLength() == 0 && list2.getLength() == 0) { 235 return true; 236 } 237 238 Collection<Element> elems1 = getElements(list1); 239 Collection<Element> elems2 = getElements(list2); 240 241 if (elems1.size() == elems2.size()) { 242 if (elems1.size() == 0) { 243 // No child elements. Let's try with text contents 244 String s1 = getText(list1); 245 String s2 = getText(list2); 246 if (s1.equals(s2)) { 247 return true; 248 } else { 249 LOG.debug("Text contents is different for elements '" + name1 250 + "' and '" + name2 + "'"); 251 return false; 252 } 253 } 254 255 loop1: 256 for (Element left : elems1) { 257 String leftName = left.getNodeName(); 258 259 for (Iterator<Element> it = elems2.iterator(); it.hasNext();) { 260 Element right = it.next(); 261 if (leftName.equals(right.getNodeName())) { 262 if (compare(left, right)) { 263 it.remove(); 264 continue loop1; 265 } 266 } 267 } 268 // Correspondence not found. Nodes are different! 269 LOG.debug("Correspondence not found for child element '" + leftName + "'"); 270 return false; 271 } 272 // Correspondence found for all child elements 273 return true; 274 } else { 275 LOG.debug("Elements '" + name1 + "' and '" + name2 276 + "' have different number of children elements"); 277 } 278 return true; 279 } 280 281 /** 282 * Compare two attribute sets. 283 * 284 * @param map1 The first node map to be compared 285 * @param map2 The second node map to be compared 286 * @return <tt>true</tt> if both nodes contain the same attribute set 287 */ 288 private boolean compare(NamedNodeMap map1, NamedNodeMap map2) { 289 290 // Check the number of attributes is the same on both nodes 291 int count1 = (map1 == null) ? 0 : map1.getLength(); 292 int count2 = (map2 == null) ? 0 : map2.getLength(); 293 if (count1 != count2) { 294 LOG.debug("Attribute set sizes are different"); 295 return false; 296 } 297 298 // Check names and values in both nodes 299 for (int i = 0; i < count1; i++) { 300 Node attr1 = map1.item(i); 301 String name = attr1.getNodeName(); 302 303 Node attr2 = map2.getNamedItem(name); 304 if (attr2 == null) { 305 LOG.debug("Attribute '" + name + "' not found in right document"); 306 return false; 307 } else if (!attr1.getNodeValue().equals(attr2.getNodeValue())) { 308 LOG.debug("Attribute '" + name + "' values are different"); 309 return false; 310 } 311 } 312 return true; 313 } 314 315 /** 316 * Gets a collection containing those nodes in the passed list whose type 317 * is 'ELEMENT'. 318 * 319 * @param nodes A NodeList 320 * @return a collection containing only the element nodes 321 */ 322 private Collection<Element> getElements(NodeList nodes) { 323 Collection<Element> children = new ArrayList<Element>(); 324 for (int i = 0; i < nodes.getLength(); i++) { 325 Node child = nodes.item(i); 326 if (child.getNodeType() == Node.ELEMENT_NODE) { 327 children.add((Element)child); 328 } 329 } 330 return children; 331 } 332 333 /** 334 * Gets a string containing the concatenation of all text nodes within the 335 * list. 336 * 337 * @param nodes A NodeList 338 * @return The text contents of the list as a single string 339 */ 340 private String getText(NodeList nodes) { 341 StringBuffer sb = new StringBuffer(); 342 for (int i = 0; i < nodes.getLength(); i++) { 343 Node child = nodes.item(i); 344 if (child.getNodeType() == Node.TEXT_NODE) { 345 sb.append(child); 346 } 347 } 348 return sb.toString(); 349 } 350 }