1   /*
2    * Copyright 2001-2004 The Apache Software Foundation.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */ 
16   
17  package org.apache.commons.betwixt.xmlunit;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.StringReader;
22  import java.io.StringWriter;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.Comparator;
26  import java.util.Iterator;
27  import java.util.List;
28  
29  import javax.xml.parsers.DocumentBuilder;
30  import javax.xml.parsers.DocumentBuilderFactory;
31  import javax.xml.parsers.ParserConfigurationException;
32  
33  import junit.framework.AssertionFailedError;
34  import junit.framework.TestCase;
35  
36  import org.apache.xerces.parsers.SAXParser;
37  import org.w3c.dom.Attr;
38  import org.w3c.dom.DOMException;
39  import org.w3c.dom.Document;
40  import org.w3c.dom.NamedNodeMap;
41  import org.w3c.dom.Node;
42  import org.w3c.dom.NodeList;
43  import org.xml.sax.InputSource;
44  import org.xml.sax.SAXException;
45  import org.xml.sax.SAXParseException;
46  import org.xml.sax.helpers.DefaultHandler;
47  
48  /*** 
49    * Provides xml test utilities. 
50    * Hopefully, these might be moved into [xmlunit] sometime.
51    *
52    * @author Robert Burrell Donkin
53    * @author Khaled Noaman, IBM (some portions derived from test code originally contributed to the Apache Xerces project)
54    */
55  public class XmlTestCase extends TestCase {
56  
57      private static final String NAMESPACES_FEATURE_ID 
58                              = "http://xml.org/sax/features/namespaces";
59  
60      private static final String NAMESPACE_PREFIXES_FEATURE_ID 
61                              = "http://xml.org/sax/features/namespace-prefixes";
62  
63      private static final String VALIDATION_FEATURE_ID 
64                              = "http://xml.org/sax/features/validation";
65  
66      private static final String SCHEMA_VALIDATION_FEATURE_ID 
67                              = "http://apache.org/xml/features/validation/schema";
68  
69      private static final String SCHEMA_FULL_CHECKING_FEATURE_ID 
70                              = "http://apache.org/xml/features/validation/schema-full-checking";
71  
72      private static final String DYNAMIC_VALIDATION_FEATURE_ID 
73                              = "http://apache.org/xml/features/validation/dynamic";
74  
75      private static final String NONAMESPACE_SCHEMA_LOCATION_PROPERTY_ID 
76          = "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation";
77  
78  
79      protected static boolean debug = false;
80  
81      DocumentBuilderFactory domFactory;
82  
83           
84      public XmlTestCase(String testName) {
85          super(testName);
86      }
87  
88      
89      public void xmlAssertIsomorphicContent(
90                                  org.w3c.dom.Document documentOne, 
91                                  org.w3c.dom.Document documentTwo)
92                                      throws 
93                                          AssertionFailedError {
94          log("Testing documents:" + documentOne.getDocumentElement().getNodeName() 
95              + " and " + documentTwo.getDocumentElement().getNodeName());
96          xmlAssertIsomorphicContent(documentOne, documentTwo, false);
97      }
98  
99      public void xmlAssertIsomorphicContent(
100                                 org.w3c.dom.Document documentOne, 
101                                 org.w3c.dom.Document documentTwo,
102                                 boolean orderIndependent)
103                                     throws 
104                                         AssertionFailedError {
105         xmlAssertIsomorphicContent(null, documentOne, documentTwo, orderIndependent);
106     }
107     
108     public void xmlAssertIsomorphicContent(
109                                 String message,
110                                 org.w3c.dom.Document documentOne, 
111                                 org.w3c.dom.Document documentTwo)
112                                     throws 
113                                         AssertionFailedError {
114         
115         xmlAssertIsomorphicContent(message, documentOne, documentTwo, false);
116     }
117     
118     public void xmlAssertIsomorphicContent(
119                                 String message,
120                                 org.w3c.dom.Document documentOne, 
121                                 org.w3c.dom.Document documentTwo,
122                                 boolean orderIndependent)
123                                     throws 
124                                         AssertionFailedError
125     {
126         // two documents have isomorphic content iff their root elements 
127         // are isomophic
128         xmlAssertIsomorphic(
129                             message, 
130                             documentOne.getDocumentElement(), 
131                             documentTwo.getDocumentElement(),
132                             orderIndependent);
133     }
134  
135     
136     public void xmlAssertIsomorphic(
137                                 org.w3c.dom.Node rootOne, 
138                                 org.w3c.dom.Node rootTwo) 
139                                     throws 
140                                         AssertionFailedError {
141         xmlAssertIsomorphic(rootOne, rootTwo, false);
142     }
143     
144     public void xmlAssertIsomorphic(
145                                 org.w3c.dom.Node rootOne, 
146                                 org.w3c.dom.Node rootTwo,
147                                 boolean orderIndependent)
148                                     throws 
149                                         AssertionFailedError
150     {
151         xmlAssertIsomorphic(null, rootOne, rootTwo, orderIndependent);
152     }
153     
154     public void xmlAssertIsomorphic(
155                                 String message,
156                                 org.w3c.dom.Node rootOne, 
157                                 org.w3c.dom.Node rootTwo) {
158                                 
159         xmlAssertIsomorphic(message, rootOne, rootTwo, false);
160     
161     }
162     
163     public void xmlAssertIsomorphic(
164                                 String message,
165                                 org.w3c.dom.Node rootOne, 
166                                 org.w3c.dom.Node rootTwo,
167                                 boolean orderIndependent)
168                                     throws 
169                                         AssertionFailedError
170     {
171         // first normalize the xml
172         rootOne.normalize();
173         rootTwo.normalize();
174         // going to use recursion so avoid normalizing each time
175         testIsomorphic(message, rootOne, rootTwo, orderIndependent);
176     }
177  
178     
179     private void testIsomorphic(
180                                 String message,
181                                 org.w3c.dom.Node nodeOne, 
182                                 org.w3c.dom.Node nodeTwo)
183                                     throws 
184                                         AssertionFailedError {
185                                         
186         testIsomorphic(message, nodeOne, nodeTwo, false);
187     }
188                                     
189     
190     private void testIsomorphic(
191                                 String message,
192                                 org.w3c.dom.Node nodeOne, 
193                                 org.w3c.dom.Node nodeTwo,
194                                 boolean orderIndependent)
195                                     throws 
196                                         AssertionFailedError
197     {
198         try {
199             if (debug) {
200                 log(
201                     "node 1 name=" + nodeOne.getNodeName() 
202                     + " qname=" + nodeOne.getLocalName());
203                 log(
204                     "node 2 name=" + nodeTwo.getNodeName() 
205                     + " qname=" + nodeTwo.getLocalName());
206             }
207             
208             // compare node properties
209             log("Comparing node properties");
210             assertEquals(
211                         (null == message ? "(Unequal node types)" : message + "(Unequal node types)"), 
212                         nodeOne.getNodeType(), 
213                         nodeTwo.getNodeType());
214             assertEquals(
215                         (null == message ? "(Unequal node names)" : message + "(Unequal node names)"), 
216                         nodeOne.getNodeName(), 
217                         nodeTwo.getNodeName());
218             assertEquals(
219                         (null == message ? "(Unequal node values)" : message + "(Unequal node values)"), 
220                         trim(nodeOne.getNodeValue()), 
221                         trim(nodeTwo.getNodeValue()));
222             assertEquals(
223                         (null == message ? "(Unequal local names)" : message + "(Unequal local names)"), 
224                         nodeOne.getLocalName(), 
225                         nodeTwo.getLocalName());
226             assertEquals(
227                         (null == message ? "(Unequal namespace)" : message + "(Unequal namespace)"), 
228                         nodeOne.getNamespaceURI(), 
229                         nodeTwo.getNamespaceURI());
230             
231                                                   
232             // compare attributes
233             log("Comparing attributes");
234             // make sure both have them first
235             assertEquals(
236                         (null == message ? "(Unequal attributes)" : message + "(Unequal attributes)"), 
237                         nodeOne.hasAttributes(), 
238                         nodeTwo.hasAttributes());            
239             if (nodeOne.hasAttributes()) {
240                 // do the actual comparison
241                 // first we check the number of attributes are equal 
242                 // we then check that for every attribute of node one, 
243                 // a corresponding attribute exists in node two
244                 // (this should be sufficient to prove equality)
245                 NamedNodeMap attributesOne = nodeOne.getAttributes();
246                 NamedNodeMap attributesTwo = nodeTwo.getAttributes();
247                 
248                 assertEquals(
249                         (null == message ? "(Unequal attributes)" : message + "(Unequal attributes)"), 
250                         attributesOne.getLength(), 
251                         attributesTwo.getLength());
252                 
253                 for (int i=0, size=attributesOne.getLength(); i<size; i++) {
254                     Attr attributeOne = (Attr) attributesOne.item(i);
255                     Attr attributeTwo = (Attr) attributesTwo.getNamedItemNS(
256                                                     attributeOne.getNamespaceURI(),
257                                                     attributeOne.getLocalName());
258                     if (attributeTwo == null) {
259                         attributeTwo = (Attr) attributesTwo.getNamedItem(attributeOne.getName());
260                     }
261                     
262                     // check attribute two exists
263                     if (attributeTwo == null) {
264                         String diagnosis = "[Missing attribute (" + attributeOne.getName() +  ")]";
265                         fail((null == message ?  diagnosis : message + diagnosis));
266                     }
267                     
268                     // now check attribute values
269                     assertEquals(
270                         (null == message ? "(Unequal attribute values)" : message + "(Unequal attribute values)"), 
271                         attributeOne.getValue(), 
272                         attributeTwo.getValue());                    
273                 }
274             }
275             
276             
277             // compare children
278             log("Comparing children");
279             // this time order is important
280             // so we can just go down the list and compare node-wise using recursion
281             List listOne = sanitize(nodeOne.getChildNodes());
282             List listTwo = sanitize(nodeTwo.getChildNodes());
283 
284             if (orderIndependent) {
285                 log("[Order Independent]");
286                 Comparator nodeByName = new NodeByNameComparator();
287                 Collections.sort(listOne, nodeByName);
288                 Collections.sort(listTwo, nodeByName);
289             }
290             
291             Iterator it = listOne.iterator();
292             Iterator iter2 = listTwo.iterator();
293             while (it.hasNext() & iter2.hasNext()) {
294                 Node nextOne = ((Node)it.next());
295                 Node nextTwo = ((Node)iter2.next());
296                 log(nextOne.getNodeName() + ":" + nextOne.getNodeValue());
297                 log(nextTwo.getNodeName() + ":" + nextTwo.getNodeValue());
298             }
299 
300             assertEquals(
301                         (null == message ? "(Unequal child nodes@" + nodeOne.getNodeName() +")": 
302                                 message + "(Unequal child nodes @" + nodeOne.getNodeName() +")"), 
303                         listOne.size(), 
304                         listTwo.size());           
305                         
306             it = listOne.iterator();
307             iter2 = listTwo.iterator();
308             while (it.hasNext() & iter2.hasNext()) {	
309                 Node nextOne = ((Node)it.next());
310                 Node nextTwo = ((Node)iter2.next());
311                 log(nextOne.getNodeName() + " vs " + nextTwo.getNodeName());
312                 testIsomorphic(message, nextOne, nextTwo, orderIndependent);
313             
314             }
315         
316         } catch (DOMException ex) {
317             fail((null == message ? "" : message + " ") + "DOM exception" + ex.toString());
318         }
319     }
320     
321     
322     protected DocumentBuilder createDocumentBuilder() {
323         try {
324 
325             return getDomFactory().newDocumentBuilder();
326         
327         } catch (ParserConfigurationException e) {
328             fail("Cannot create DOM builder: " + e.toString());
329         
330         }
331         // just to keep the compiler happy
332         return null;
333     }
334     
335     protected DocumentBuilderFactory getDomFactory() {
336         // lazy creation
337         if (domFactory == null) {
338             domFactory = DocumentBuilderFactory.newInstance();
339         }
340         
341         return domFactory;
342     }
343     
344     protected Document parseString(StringWriter writer) {
345         try { 
346         
347             return createDocumentBuilder().parse(new InputSource(new StringReader(writer.getBuffer().toString())));
348         
349         } catch (SAXException e) {
350             fail("Cannot create parse string: " + e.toString());
351         
352         } catch (IOException e) {
353             fail("Cannot create parse string: " + e.toString());
354         
355         } 
356         // just to keep the compiler happy
357         return null;
358     }
359     
360     protected Document parseString(String string) {
361         try { 
362         
363             return createDocumentBuilder().parse(new InputSource(new StringReader(string)));
364         
365         } catch (SAXException e) {
366             fail("Cannot create parse string: " + e.toString());
367         
368         } catch (IOException e) {
369             fail("Cannot create parse string: " + e.toString());
370         
371         } 
372         // just to keep the compiler happy
373         return null;
374     }
375 
376     
377     protected Document parseFile(String path) {
378         try { 
379         
380             return createDocumentBuilder().parse(new File(path));
381         
382         } catch (SAXException e) {
383             fail("Cannot create parse file: " + e.toString());
384         
385         } catch (IOException e) {
386             fail("Cannot create parse file: " + e.toString());
387         
388         } 
389         // just to keep the compiler happy
390         return null;
391     }
392     
393     private void log(String message)
394     {
395         if (debug) {
396             System.out.println("[XmlTestCase]" + message);
397         }
398     }
399 
400     
401     private void log(String message, Exception e)
402     {
403         if (debug) {
404             System.out.println("[XmlTestCase]" + message);
405             e.printStackTrace();
406         }
407     }
408     
409     private String trim(String trimThis)
410     {
411         if (trimThis == null) {
412             return trimThis;
413         }
414         
415         return trimThis.trim();
416     }
417     
418     private List sanitize(NodeList nodes) {
419         ArrayList list = new ArrayList();
420         
421         for (int i=0, size=nodes.getLength(); i<size; i++) {
422             if ( nodes.item(i).getNodeType() == Node.TEXT_NODE ) {
423                 if ( !( nodes.item(i).getNodeValue() == null ||  
424                         nodes.item(i).getNodeValue().trim().length() == 0 )) {
425                     list.add(nodes.item(i));
426                 } else {
427                     log("Ignoring text node:" + nodes.item(i).getNodeValue());
428                 }
429             } else {
430                 list.add(nodes.item(i));
431             }
432         }
433         return list;
434     }
435     
436     private class NodeByNameComparator implements Comparator {
437         public int compare(Object objOne, Object objTwo) {
438             String nameOne = ((Node) objOne).getNodeName();
439             String nameTwo = ((Node) objTwo).getNodeName();
440             
441             if (nameOne == null) {
442                 if (nameTwo == null) {
443                     return 0;
444                 }
445                 return -1;
446             }
447             
448             if (nameTwo == null) {
449                 return 1;
450             }
451             
452             return nameOne.compareTo(nameTwo);
453         }
454     }
455     
456     
457     public void validateWithSchema(InputSource documentSource, final InputSource schemaSource) 
458             throws ParserConfigurationException, SAXException, IOException
459     {
460         class XMLUnitHandler extends DefaultHandler {
461             ArrayList errors = new ArrayList();
462             ArrayList warnings = new ArrayList();
463             InputSource schemaSource;
464     
465             XMLUnitHandler(InputSource schemaSource) {
466                 this.schemaSource = schemaSource;
467                 schemaSource.setSystemId("schema.xsd");
468             }
469     
470             public InputSource resolveEntity(String publicId, String systemId) {
471                 return schemaSource;
472             }
473     
474             public void error(SAXParseException ex) {
475                 errors.add(ex);
476             }
477     
478             public void warning(SAXParseException ex) {
479                 warnings.add(ex);
480             }
481     
482             void reportErrors() throws SAXException {
483                 if (errors.size() > 0) {
484                     throw (SAXException) errors.get(0);
485                 }
486             }
487     
488         }
489 
490         // it's not all that good to have a concrete dependency on Xerces
491         // and a particular version, at that.
492         // but schema support in the Xerces series of parsers is variable
493         // and some of the configuration details differ.
494         // At least this way seems reliable
495         SAXParser parser = new SAXParser();
496     
497         // Set features
498         parser.setFeature(NAMESPACES_FEATURE_ID, true);
499         parser.setFeature(NAMESPACE_PREFIXES_FEATURE_ID, false);
500         parser.setFeature(VALIDATION_FEATURE_ID, true);
501         parser.setFeature(SCHEMA_VALIDATION_FEATURE_ID, true);
502         parser.setFeature(SCHEMA_FULL_CHECKING_FEATURE_ID, false);
503         parser.setFeature(DYNAMIC_VALIDATION_FEATURE_ID, false);
504     
505         // Set properties
506         parser.setProperty(NONAMESPACE_SCHEMA_LOCATION_PROPERTY_ID, "schema.xsd");
507     
508         XMLUnitHandler handler = new XMLUnitHandler(schemaSource);
509     
510         // Set handlers
511         parser.setContentHandler(handler);
512         parser.setErrorHandler(handler);
513         parser.setEntityResolver(handler);
514     
515         // parse document
516         parser.parse(documentSource);
517         handler.reportErrors();
518     }
519     
520     public boolean isValid(InputSource documentSource, InputSource schemaSource) 
521             throws ParserConfigurationException, IOException
522     {
523         boolean result = false;
524         try
525         {
526             validateWithSchema(documentSource, schemaSource);
527             result = true;
528         }
529         catch (SAXException se)
530         {
531             log("Validation failed.", se);
532         }
533         
534         return result;
535     }
536     
537     
538     public void xmlAssertIsValid(String document, String schema) 
539         throws ParserConfigurationException, IOException
540     {
541         xmlAssertIsValid(new InputSource(new StringReader(document)), new InputSource(new StringReader(schema))); 
542     }
543     
544     public void xmlAssertIsValid(InputSource documentSource, InputSource schemaSource) 
545         throws ParserConfigurationException, IOException
546     {
547         try
548         {
549             validateWithSchema(documentSource, schemaSource);
550         }
551         catch (SAXException se)
552         {
553             se.printStackTrace();
554             fail("Validation failure: " + se.getMessage());
555         }   
556     }
557 }
558