View Javadoc

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   *   &lt;field>
64   *     &lt;value>123&lt;/value>
65   *   &lt;/field>
66   *
67   *   &ltfield>&lt;value>123&lt;/value>&lt;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 }