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