001: /*
002: ******************************************************************
003: Copyright (c) 2001-2007, Jeff Martin, Tim Bacon
004: All rights reserved.
005:
006: Redistribution and use in source and binary forms, with or without
007: modification, are permitted provided that the following conditions
008: are met:
009:
010: * Redistributions of source code must retain the above copyright
011: notice, this list of conditions and the following disclaimer.
012: * Redistributions in binary form must reproduce the above
013: copyright notice, this list of conditions and the following
014: disclaimer in the documentation and/or other materials provided
015: with the distribution.
016: * Neither the name of the xmlunit.sourceforge.net nor the names
017: of its contributors may be used to endorse or promote products
018: derived from this software without specific prior written
019: permission.
020:
021: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
022: "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
023: LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
024: FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
025: COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
026: INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
027: BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
028: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
029: CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
030: LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
031: ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
032: POSSIBILITY OF SUCH DAMAGE.
033:
034: ******************************************************************
035: */
036:
037: package org.custommonkey.xmlunit;
038:
039: import java.util.ArrayList;
040: import java.util.HashMap;
041: import java.util.Iterator;
042: import java.util.List;
043:
044: import org.w3c.dom.Attr;
045: import org.w3c.dom.CharacterData;
046: import org.w3c.dom.CDATASection;
047: import org.w3c.dom.Comment;
048: import org.w3c.dom.Document;
049: import org.w3c.dom.DocumentType;
050: import org.w3c.dom.Element;
051: import org.w3c.dom.NamedNodeMap;
052: import org.w3c.dom.Node;
053: import org.w3c.dom.NodeList;
054: import org.w3c.dom.ProcessingInstruction;
055: import org.w3c.dom.Text;
056:
057: /**
058: * Class that has responsibility for comparing Nodes and notifying a
059: * DifferenceListener of any differences or dissimilarities that are found.
060: * Knows how to compare namespaces and nested child nodes, but currently
061: * only compares nodes of type ELEMENT_NODE, CDATA_SECTION_NODE,
062: * COMMENT_NODE, DOCUMENT_TYPE_NODE, PROCESSING_INSTRUCTION_NODE and TEXT_NODE.
063: * Nodes of other types (eg ENTITY_NODE) will be skipped.
064: * <br />Examples and more at <a href="http://xmlunit.sourceforge.net"/>xmlunit.
065: * sourceforge.net</a>
066: * @see DifferenceListener#differenceFound(Difference)
067: */
068: public class DifferenceEngine implements DifferenceConstants {
069: private static final String NULL_NODE = "null";
070: private static final String NOT_NULL_NODE = "not null";
071: private static final String ATTRIBUTE_ABSENT = "[attribute absent]";
072: private final ComparisonController controller;
073: private final XpathNodeTracker controlTracker;
074: private final XpathNodeTracker testTracker;
075:
076: /**
077: * Simple constructor
078: * @param controller the instance used to determine whether a Difference
079: * detected by this class should halt further comparison or not
080: * @see ComparisonController#haltComparison(Difference)
081: */
082: public DifferenceEngine(ComparisonController controller) {
083: this .controller = controller;
084: this .controlTracker = new XpathNodeTracker();
085: this .testTracker = new XpathNodeTracker();
086: }
087:
088: /**
089: * Entry point for Node comparison testing.
090: * @param control Control XML to compare
091: * @param test Test XML to compare
092: * @param listener Notified of any {@link Difference differences} detected
093: * during node comparison testing
094: * @param elementQualifier Used to determine which elements qualify for
095: * comparison e.g. when a node has repeated child elements that may occur
096: * in any sequence and that sequence is not considered important.
097: */
098: public void compare(Node control, Node test,
099: DifferenceListener listener,
100: ElementQualifier elementQualifier) {
101: controlTracker.reset();
102: testTracker.reset();
103: try {
104: compare(getNullOrNotNull(control), getNullOrNotNull(test),
105: control, test, listener, NODE_TYPE);
106: if (control != null) {
107: compareNode(control, test, listener, elementQualifier);
108: }
109: } catch (DifferenceFoundException e) {
110: // thrown by the protected compare() method to terminate the
111: // comparison and unwind the call stack back to here
112: }
113: }
114:
115: private String getNullOrNotNull(Node aNode) {
116: return aNode == null ? NULL_NODE : NOT_NULL_NODE;
117: }
118:
119: /**
120: * First point of call: if nodes are comparable it compares node values then
121: * recurses to compare node children.
122: * @param control
123: * @param test
124: * @param listener
125: * @param elementQualifier
126: * @throws DifferenceFoundException
127: */
128: protected void compareNode(Node control, Node test,
129: DifferenceListener listener,
130: ElementQualifier elementQualifier)
131: throws DifferenceFoundException {
132: boolean comparable = compareNodeBasics(control, test, listener);
133: boolean isDocumentNode = false;
134:
135: if (comparable) {
136: switch (control.getNodeType()) {
137: case Node.ELEMENT_NODE:
138: compareElement((Element) control, (Element) test,
139: listener);
140: break;
141: case Node.CDATA_SECTION_NODE:
142: case Node.TEXT_NODE:
143: compareText((CharacterData) control,
144: (CharacterData) test, listener);
145: break;
146: case Node.COMMENT_NODE:
147: compareComment((Comment) control, (Comment) test,
148: listener);
149: break;
150: case Node.DOCUMENT_TYPE_NODE:
151: compareDocumentType((DocumentType) control,
152: (DocumentType) test, listener);
153: break;
154: case Node.PROCESSING_INSTRUCTION_NODE:
155: compareProcessingInstruction(
156: (ProcessingInstruction) control,
157: (ProcessingInstruction) test, listener);
158: break;
159: case Node.DOCUMENT_NODE:
160: isDocumentNode = true;
161: compareDocument((Document) control, (Document) test,
162: listener, elementQualifier);
163: break;
164: default:
165: listener.skippedComparison(control, test);
166: }
167: }
168:
169: compareHasChildNodes(control, test, listener);
170: if (isDocumentNode) {
171: Element controlElement = ((Document) control)
172: .getDocumentElement();
173: Element testElement = ((Document) test)
174: .getDocumentElement();
175: if (controlElement != null && testElement != null) {
176: compareNode(controlElement, testElement, listener,
177: elementQualifier);
178: }
179: } else {
180: controlTracker.indent();
181: testTracker.indent();
182: compareNodeChildren(control, test, listener,
183: elementQualifier);
184: controlTracker.outdent();
185: testTracker.outdent();
186: }
187: }
188:
189: /**
190: * Compare two Documents for doctype and then element differences
191: * @param control
192: * @param test
193: * @param listener
194: * @param elementQualifier
195: * @throws DifferenceFoundException
196: */
197: protected void compareDocument(Document control, Document test,
198: DifferenceListener listener,
199: ElementQualifier elementQualifier)
200: throws DifferenceFoundException {
201: DocumentType controlDoctype = control.getDoctype();
202: DocumentType testDoctype = test.getDoctype();
203: compare(getNullOrNotNull(controlDoctype),
204: getNullOrNotNull(testDoctype), controlDoctype,
205: testDoctype, listener, HAS_DOCTYPE_DECLARATION);
206: if (controlDoctype != null && testDoctype != null) {
207: compareNode(controlDoctype, testDoctype, listener,
208: elementQualifier);
209: }
210: }
211:
212: /**
213: * Compares node type and node namespace characteristics: basically
214: * determines if nodes are comparable further
215: * @param control
216: * @param test
217: * @param listener
218: * @return true if the nodes are comparable further, false otherwise
219: * @throws DifferenceFoundException
220: */
221: protected boolean compareNodeBasics(Node control, Node test,
222: DifferenceListener listener)
223: throws DifferenceFoundException {
224: controlTracker.visited(control);
225: testTracker.visited(test);
226:
227: Short controlType = new Short(control.getNodeType());
228: Short testType = new Short(test.getNodeType());
229:
230: boolean textAndCDATA = comparingTextAndCDATA(control
231: .getNodeType(), test.getNodeType());
232: if (!textAndCDATA) {
233: compare(controlType, testType, control, test, listener,
234: NODE_TYPE);
235: }
236: compare(control.getNamespaceURI(), test.getNamespaceURI(),
237: control, test, listener, NAMESPACE_URI);
238: compare(control.getPrefix(), test.getPrefix(), control, test,
239: listener, NAMESPACE_PREFIX);
240:
241: return textAndCDATA || controlType.equals(testType);
242: }
243:
244: private boolean comparingTextAndCDATA(short controlType,
245: short testType) {
246: return XMLUnit.getIgnoreDiffBetweenTextAndCDATA()
247: && (controlType == Node.TEXT_NODE
248: && testType == Node.CDATA_SECTION_NODE || testType == Node.TEXT_NODE
249: && controlType == Node.CDATA_SECTION_NODE);
250: }
251:
252: /**
253: * Compare the number of children, and if the same, compare the actual
254: * children via their NodeLists.
255: * @param control
256: * @param test
257: * @param listener
258: * @throws DifferenceFoundException
259: */
260: protected void compareHasChildNodes(Node control, Node test,
261: DifferenceListener listener)
262: throws DifferenceFoundException {
263: Boolean controlHasChildren = hasChildNodes(control);
264: Boolean testHasChildren = hasChildNodes(test);
265: compare(controlHasChildren, testHasChildren, control, test,
266: listener, HAS_CHILD_NODES);
267: }
268:
269: /**
270: * Tests whether a Node has children, taking ignoreComments
271: * setting into account.
272: */
273: private Boolean hasChildNodes(Node n) {
274: boolean flag = n.hasChildNodes();
275: if (flag && XMLUnit.getIgnoreComments()) {
276: List nl = nodeList2List(n.getChildNodes());
277: flag = !nl.isEmpty();
278: }
279: return flag ? Boolean.TRUE : Boolean.FALSE;
280: }
281:
282: /**
283: * Returns the NodeList's Nodes as List, taking ignoreComments
284: * into account.
285: */
286: static List nodeList2List(NodeList nl) {
287: int len = nl.getLength();
288: ArrayList l = new ArrayList(len);
289: for (int i = 0; i < len; i++) {
290: Node n = nl.item(i);
291: if (!XMLUnit.getIgnoreComments() || !(n instanceof Comment)) {
292: l.add(n);
293: }
294: }
295: return l;
296: }
297:
298: /**
299: * Compare the number of children, and if the same, compare the actual
300: * children via their NodeLists.
301: * @param control
302: * @param test
303: * @param listener
304: * @param elementQualifier
305: * @throws DifferenceFoundException
306: */
307: protected void compareNodeChildren(Node control, Node test,
308: DifferenceListener listener,
309: ElementQualifier elementQualifier)
310: throws DifferenceFoundException {
311: if (control.hasChildNodes() && test.hasChildNodes()) {
312: List controlChildren = nodeList2List(control
313: .getChildNodes());
314: List testChildren = nodeList2List(test.getChildNodes());
315:
316: Integer controlLength = new Integer(controlChildren.size());
317: Integer testLength = new Integer(testChildren.size());
318: compare(controlLength, testLength, control, test, listener,
319: CHILD_NODELIST_LENGTH);
320: compareNodeList(controlChildren, testChildren,
321: controlLength.intValue(), listener,
322: elementQualifier);
323: }
324: }
325:
326: /**
327: * Compare the contents of two node list one by one, assuming that order
328: * of children is NOT important: matching begins at same position in test
329: * list as control list.
330: * @param control
331: * @param test
332: * @param numNodes convenience parameter because the calling method should
333: * know the value already
334: * @param listener
335: * @param elementQualifier used to determine which of the child elements in
336: * the test NodeList should be compared to the current child element in the
337: * control NodeList.
338: * @throws DifferenceFoundException
339: * @deprecated Use the version with List arguments instead
340: */
341: protected void compareNodeList(final NodeList control,
342: final NodeList test, final int numNodes,
343: final DifferenceListener listener,
344: final ElementQualifier elementQualifier)
345: throws DifferenceFoundException {
346: compareNodeList(nodeList2List(control), nodeList2List(test),
347: numNodes, listener, elementQualifier);
348: }
349:
350: /**
351: * Compare the contents of two node list one by one, assuming that order
352: * of children is NOT important: matching begins at same position in test
353: * list as control list.
354: * @param control
355: * @param test
356: * @param numNodes convenience parameter because the calling method should
357: * know the value already
358: * @param listener
359: * @param elementQualifier used to determine which of the child elements in
360: * the test NodeList should be compared to the current child element in the
361: * control NodeList.
362: * @throws DifferenceFoundException
363: */
364: protected void compareNodeList(final List controlChildren,
365: final List testChildren, final int numNodes,
366: final DifferenceListener listener,
367: final ElementQualifier elementQualifier)
368: throws DifferenceFoundException {
369:
370: int j = 0;
371: final int lastTestNode = testChildren.size() - 1;
372: testTracker.preloadChildList(testChildren);
373:
374: HashMap/*<Node, Node>*/matchingNodes = new HashMap();
375: HashMap/*<Node, Integer>*/matchingNodeIndexes = new HashMap();
376:
377: List/*<Node>*/unmatchedTestNodes = new ArrayList(testChildren);
378:
379: // first pass to find the matching nodes in control and test docs
380: for (int i = 0; i < numNodes; ++i) {
381: Node nextControl = (Node) controlChildren.get(i);
382: boolean matchOnElement = nextControl instanceof Element;
383: short findNodeType = nextControl.getNodeType();
384: int startAt = (i > lastTestNode ? lastTestNode : i);
385: j = startAt;
386:
387: boolean matchFound = false;
388:
389: while (!matchFound) {
390: Node t = (Node) testChildren.get(j);
391: if (findNodeType == t.getNodeType()
392: || comparingTextAndCDATA(findNodeType, t
393: .getNodeType())) {
394: matchFound = !matchOnElement
395: || elementQualifier == null
396: || elementQualifier.qualifyForComparison(
397: (Element) nextControl, (Element) t);
398: }
399: if (!matchFound) {
400: ++j;
401: if (j > lastTestNode) {
402: j = 0;
403: }
404: if (j == startAt) {
405: // been through all children
406: break;
407: }
408: }
409: }
410: if (matchFound) {
411: matchingNodes.put(nextControl, testChildren.get(j));
412: matchingNodeIndexes.put(nextControl, new Integer(j));
413: unmatchedTestNodes.remove(testChildren.get(j));
414: }
415: }
416:
417: // next, do the actual comparision on those that matched - or
418: // match them against the first test nodes that didn't match
419: // any other control nodes
420: for (int i = 0; i < numNodes; ++i) {
421: Node nextControl = (Node) controlChildren.get(i);
422: Node nextTest = (Node) matchingNodes.get(nextControl);
423: Integer testIndex = (Integer) matchingNodeIndexes
424: .get(nextControl);
425: if (nextTest == null && !unmatchedTestNodes.isEmpty()) {
426: nextTest = (Node) unmatchedTestNodes.get(0);
427: testIndex = new Integer(testChildren.indexOf(nextTest));
428: unmatchedTestNodes.remove(0);
429: }
430: if (nextTest != null) {
431: compareNode(nextControl, nextTest, listener,
432: elementQualifier);
433: compare(new Integer(i), testIndex, nextControl,
434: nextTest, listener, CHILD_NODELIST_SEQUENCE);
435: } else {
436: compare(nextControl.getNodeName(), null, nextControl,
437: null, listener, CHILD_NODE_NOT_FOUND);
438: }
439: }
440:
441: // now handle remaining unmatched test nodes
442: for (Iterator iter = unmatchedTestNodes.iterator(); iter
443: .hasNext();) {
444: Node n = (Node) iter.next();
445: compare(null, n.getNodeName(), null, n, listener,
446: CHILD_NODE_NOT_FOUND);
447: }
448: }
449:
450: /**
451: * @param aNode
452: * @return true if the node has a namespace
453: */
454: private boolean isNamespaced(Node aNode) {
455: String namespace = aNode.getNamespaceURI();
456: return namespace != null && namespace.length() > 0;
457: }
458:
459: /**
460: * Compare 2 elements and their attributes
461: * @param control
462: * @param test
463: * @param listener
464: * @throws DifferenceFoundException
465: */
466: protected void compareElement(Element control, Element test,
467: DifferenceListener listener)
468: throws DifferenceFoundException {
469: compare(getUnNamespacedNodeName(control),
470: getUnNamespacedNodeName(test), control, test, listener,
471: ELEMENT_TAG_NAME);
472:
473: NamedNodeMap controlAttr = control.getAttributes();
474: Integer controlNonXmlnsAttrLength = getNonSpecialAttrLength(controlAttr);
475: NamedNodeMap testAttr = test.getAttributes();
476: Integer testNonXmlnsAttrLength = getNonSpecialAttrLength(testAttr);
477: compare(controlNonXmlnsAttrLength, testNonXmlnsAttrLength,
478: control, test, listener, ELEMENT_NUM_ATTRIBUTES);
479:
480: compareElementAttributes(control, test, controlAttr, testAttr,
481: listener);
482: }
483:
484: /**
485: * The number of attributes not related to namespace declarations
486: * and/or Schema location.
487: */
488: private Integer getNonSpecialAttrLength(NamedNodeMap attributes) {
489: int length = 0, maxLength = attributes.getLength();
490: for (int i = 0; i < maxLength; ++i) {
491: Attr a = (Attr) attributes.item(i);
492: if (!isXMLNSAttribute(a)
493: && !isRecognizedXMLSchemaInstanceAttribute(a)) {
494: ++length;
495: }
496: }
497: return new Integer(length);
498: }
499:
500: void compareElementAttributes(Element control, Element test,
501: NamedNodeMap controlAttr, NamedNodeMap testAttr,
502: DifferenceListener listener)
503: throws DifferenceFoundException {
504: ArrayList unmatchedTestAttrs = new ArrayList();
505: for (int i = 0; i < testAttr.getLength(); ++i) {
506: Attr nextAttr = (Attr) testAttr.item(i);
507: if (!isXMLNSAttribute(nextAttr)) {
508: unmatchedTestAttrs.add(nextAttr);
509: }
510: }
511:
512: for (int i = 0; i < controlAttr.getLength(); ++i) {
513: Attr nextAttr = (Attr) controlAttr.item(i);
514: if (isXMLNSAttribute(nextAttr)) {
515: // xml namespacing is handled in compareNodeBasics
516: } else {
517: boolean isNamespacedAttr = isNamespaced(nextAttr);
518: String attrName = getUnNamespacedNodeName(nextAttr,
519: isNamespacedAttr);
520: Attr compareTo = null;
521:
522: if (isNamespacedAttr) {
523: compareTo = (Attr) testAttr.getNamedItemNS(nextAttr
524: .getNamespaceURI(), attrName);
525: } else {
526: compareTo = (Attr) testAttr.getNamedItem(attrName);
527: }
528:
529: if (compareTo != null) {
530: unmatchedTestAttrs.remove(compareTo);
531: }
532:
533: if (isRecognizedXMLSchemaInstanceAttribute(nextAttr)) {
534: compareRecognizedXMLSchemaInstanceAttribute(
535: nextAttr, compareTo, listener);
536:
537: } else if (compareTo != null) {
538: compareAttribute(nextAttr, compareTo, listener);
539:
540: if (!XMLUnit.getIgnoreAttributeOrder()) {
541: Attr attributeItem = (Attr) testAttr.item(i);
542: String testAttrName = ATTRIBUTE_ABSENT;
543: if (attributeItem != null) {
544: testAttrName = getUnNamespacedNodeName(attributeItem);
545: }
546: compare(attrName, testAttrName, nextAttr,
547: compareTo, listener, ATTR_SEQUENCE);
548: }
549: } else {
550: compare(attrName, null, control, test, listener,
551: ATTR_NAME_NOT_FOUND);
552: }
553: }
554: }
555:
556: for (Iterator iter = unmatchedTestAttrs.iterator(); iter
557: .hasNext();) {
558: Attr nextAttr = (Attr) iter.next();
559: if (isRecognizedXMLSchemaInstanceAttribute(nextAttr)) {
560: compareRecognizedXMLSchemaInstanceAttribute(null,
561: nextAttr, listener);
562: } else {
563: compare(null, getUnNamespacedNodeName(nextAttr,
564: isNamespaced(nextAttr)), control, test,
565: listener, ATTR_NAME_NOT_FOUND);
566: }
567: }
568:
569: controlTracker.clearTrackedAttribute();
570: testTracker.clearTrackedAttribute();
571: }
572:
573: private String getUnNamespacedNodeName(Node aNode) {
574: return getUnNamespacedNodeName(aNode, isNamespaced(aNode));
575: }
576:
577: private String getUnNamespacedNodeName(Node aNode,
578: boolean isNamespacedNode) {
579: if (isNamespacedNode) {
580: return aNode.getLocalName();
581: }
582: return aNode.getNodeName();
583: }
584:
585: /**
586: * @param attribute
587: * @return true if the attribute represents a namespace declaration
588: */
589: private boolean isXMLNSAttribute(Attr attribute) {
590: return XMLConstants.XMLNS_PREFIX.equals(attribute.getPrefix())
591: || XMLConstants.XMLNS_PREFIX
592: .equals(attribute.getName());
593: }
594:
595: /**
596: * @param attr
597: * @return true if the attribute is an XML Schema Instance
598: * namespace attribute XMLUnit treats in a special way.
599: */
600: private boolean isRecognizedXMLSchemaInstanceAttribute(Attr attr) {
601: return XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(attr
602: .getNamespaceURI())
603: && (XMLConstants.W3C_XML_SCHEMA_INSTANCE_SCHEMA_LOCATION_ATTR
604: .equals(attr.getLocalName()) || XMLConstants.W3C_XML_SCHEMA_INSTANCE_NO_NAMESPACE_SCHEMA_LOCATION_ATTR
605: .equals(attr.getLocalName()));
606: }
607:
608: /**
609: * Compare two attributes
610: * @param control
611: * @param test
612: * @param listener
613: * @throws DifferenceFoundException
614: */
615: protected void compareRecognizedXMLSchemaInstanceAttribute(
616: Attr control, Attr test, DifferenceListener listener)
617: throws DifferenceFoundException {
618: Attr nonNullNode = control != null ? control : test;
619: Difference d = XMLConstants.W3C_XML_SCHEMA_INSTANCE_SCHEMA_LOCATION_ATTR
620: .equals(nonNullNode.getLocalName()) ? SCHEMA_LOCATION
621: : NO_NAMESPACE_SCHEMA_LOCATION;
622:
623: if (control != null) {
624: controlTracker.visited(control);
625: }
626: if (test != null) {
627: testTracker.visited(test);
628: }
629:
630: compare(
631: control != null ? control.getValue() : ATTRIBUTE_ABSENT,
632: test != null ? test.getValue() : ATTRIBUTE_ABSENT,
633: control, test, listener, d);
634: }
635:
636: /**
637: * Compare two attributes
638: * @param control
639: * @param test
640: * @param listener
641: * @throws DifferenceFoundException
642: */
643: protected void compareAttribute(Attr control, Attr test,
644: DifferenceListener listener)
645: throws DifferenceFoundException {
646: controlTracker.visited(control);
647: testTracker.visited(test);
648:
649: compare(control.getPrefix(), test.getPrefix(), control, test,
650: listener, NAMESPACE_PREFIX);
651:
652: compare(control.getValue(), test.getValue(), control, test,
653: listener, ATTR_VALUE);
654:
655: compare(control.getSpecified() ? Boolean.TRUE : Boolean.FALSE,
656: test.getSpecified() ? Boolean.TRUE : Boolean.FALSE,
657: control, test, listener,
658: ATTR_VALUE_EXPLICITLY_SPECIFIED);
659: }
660:
661: /**
662: * Compare two CDATA sections - unused, kept for backwards compatibility
663: * @param control
664: * @param test
665: * @param listener
666: * @throws DifferenceFoundException
667: */
668: protected void compareCDataSection(CDATASection control,
669: CDATASection test, DifferenceListener listener)
670: throws DifferenceFoundException {
671: compareText(control, test, listener);
672: }
673:
674: /**
675: * Compare two comments
676: * @param control
677: * @param test
678: * @param listener
679: * @throws DifferenceFoundException
680: */
681: protected void compareComment(Comment control, Comment test,
682: DifferenceListener listener)
683: throws DifferenceFoundException {
684: if (!XMLUnit.getIgnoreComments()) {
685: compareCharacterData(control, test, listener, COMMENT_VALUE);
686: }
687: }
688:
689: /**
690: * Compare two DocumentType nodes
691: * @param control
692: * @param test
693: * @param listener
694: * @throws DifferenceFoundException
695: */
696: protected void compareDocumentType(DocumentType control,
697: DocumentType test, DifferenceListener listener)
698: throws DifferenceFoundException {
699: compare(control.getName(), test.getName(), control, test,
700: listener, DOCTYPE_NAME);
701: compare(control.getPublicId(), test.getPublicId(), control,
702: test, listener, DOCTYPE_PUBLIC_ID);
703:
704: compare(control.getSystemId(), test.getSystemId(), control,
705: test, listener, DOCTYPE_SYSTEM_ID);
706: }
707:
708: /**
709: * Compare two processing instructions
710: * @param control
711: * @param test
712: * @param listener
713: * @throws DifferenceFoundException
714: */
715: protected void compareProcessingInstruction(
716: ProcessingInstruction control, ProcessingInstruction test,
717: DifferenceListener listener)
718: throws DifferenceFoundException {
719: compare(control.getTarget(), test.getTarget(), control, test,
720: listener, PROCESSING_INSTRUCTION_TARGET);
721: compare(control.getData(), test.getData(), control, test,
722: listener, PROCESSING_INSTRUCTION_DATA);
723: }
724:
725: /**
726: * Compare text - unused, kept for backwards compatibility
727: * @param control
728: * @param test
729: * @param listener
730: * @throws DifferenceFoundException
731: */
732: protected void compareText(Text control, Text test,
733: DifferenceListener listener)
734: throws DifferenceFoundException {
735: compareText((CharacterData) control, (CharacterData) test,
736: listener);
737: }
738:
739: /**
740: * Compare text
741: * @param control
742: * @param test
743: * @param listener
744: * @throws DifferenceFoundException
745: */
746: protected void compareText(CharacterData control,
747: CharacterData test, DifferenceListener listener)
748: throws DifferenceFoundException {
749: compareCharacterData(control, test, listener,
750: control instanceof CDATASection ? CDATA_VALUE
751: : TEXT_VALUE);
752: }
753:
754: /**
755: * Character comparison method used by comments, text and CDATA sections
756: * @param control
757: * @param test
758: * @param listener
759: * @param differenceType
760: * @throws DifferenceFoundException
761: */
762: private void compareCharacterData(CharacterData control,
763: CharacterData test, DifferenceListener listener,
764: Difference difference) throws DifferenceFoundException {
765: compare(control.getData(), test.getData(), control, test,
766: listener, difference);
767: }
768:
769: /**
770: * If the expected and actual values are unequal then inform the listener of
771: * a difference and throw a DifferenceFoundException.
772: * @param expected
773: * @param actual
774: * @param control
775: * @param test
776: * @param listener
777: * @param differenceType
778: * @throws DifferenceFoundException
779: */
780: protected void compare(Object expected, Object actual,
781: Node control, Node test, DifferenceListener listener,
782: Difference difference) throws DifferenceFoundException {
783: if (unequal(expected, actual)) {
784: NodeDetail controlDetail = new NodeDetail(String
785: .valueOf(expected), control, controlTracker
786: .toXpathString());
787: NodeDetail testDetail = new NodeDetail(String
788: .valueOf(actual), test, testTracker.toXpathString());
789: Difference differenceInstance = new Difference(difference,
790: controlDetail, testDetail);
791: listener.differenceFound(differenceInstance);
792: if (controller.haltComparison(differenceInstance)) {
793: throw flowControlException;
794: }
795: }
796: }
797:
798: /**
799: * Test two possibly null values for inequality
800: * @param expected
801: * @param actual
802: * @return TRUE if the values are neither both null, nor equals() equal
803: */
804: private boolean unequal(Object expected, Object actual) {
805: return (expected == null ? actual != null : unequalNotNull(
806: expected, actual));
807: }
808:
809: /**
810: * Test two non-null values for inequality
811: * @param expected
812: * @param actual
813: * @return TRUE if the values are not equals() equal (taking whitespace
814: * into account if necessary)
815: */
816: private boolean unequalNotNull(Object expected, Object actual) {
817: if ((XMLUnit.getIgnoreWhitespace() || XMLUnit
818: .getNormalizeWhitespace())
819: && expected instanceof String
820: && actual instanceof String) {
821: String expectedString = ((String) expected).trim();
822: String actualString = ((String) actual).trim();
823: if (XMLUnit.getNormalizeWhitespace()) {
824: expectedString = normalizeWhitespace(expectedString);
825: actualString = normalizeWhitespace(actualString);
826: }
827: return !expectedString.equals(actualString);
828: }
829: return !(expected.equals(actual));
830: }
831:
832: /**
833: * Replace all whitespace characters with SPACE and collapse
834: * consecutive whitespace chars to a single SPACE.
835: */
836: final static String normalizeWhitespace(String orig) {
837: StringBuffer sb = new StringBuffer();
838: boolean lastCharWasWhitespace = false;
839: boolean changed = false;
840: char[] characters = orig.toCharArray();
841: for (int i = 0; i < characters.length; i++) {
842: if (Character.isWhitespace(characters[i])) {
843: if (lastCharWasWhitespace) {
844: // suppress character
845: changed = true;
846: } else {
847: sb.append(' ');
848: changed |= characters[i] != ' ';
849: lastCharWasWhitespace = true;
850: }
851: } else {
852: sb.append(characters[i]);
853: lastCharWasWhitespace = false;
854: }
855: }
856: return changed ? sb.toString() : orig;
857: }
858:
859: /**
860: * Marker exception thrown by the protected compare() method and passed
861: * upwards through the call stack to the public compare() method.
862: */
863: protected static final class DifferenceFoundException extends
864: Exception {
865: private DifferenceFoundException() {
866: super ("This exception is used to control flow");
867: }
868: }
869:
870: /**
871: * Exception instance used internally to control flow
872: * when a difference is found
873: */
874: private static final DifferenceFoundException flowControlException = new DifferenceFoundException();
875: }
|