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 }