• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xmlunit / xmlunit / cd160610-9b67-4752-a2c4-e3a0d82d991e

21 Apr 2025 11:55AM UTC coverage: 91.756% (-0.02%) from 91.78%
cd160610-9b67-4752-a2c4-e3a0d82d991e

push

circleci

web-flow
Merge pull request #289 from xmlunit/circleci-project-setup

CircleCI project setup

3996 of 4698 branches covered (85.06%)

11754 of 12810 relevant lines covered (91.76%)

2.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.72
/xmlunit-legacy/src/main/java/org/custommonkey/xmlunit/DifferenceEngine.java
1
/*
2
******************************************************************
3
Copyright (c) 2001-2010,2013,2015-2016,2022 Jeff Martin, Tim Bacon
4
All rights reserved.
5

6
Redistribution and use in source and binary forms, with or without
7
modification, are permitted provided that the following conditions
8
are met:
9

10
    * Redistributions of source code must retain the above copyright
11
      notice, this list of conditions and the following disclaimer.
12
    * Redistributions in binary form must reproduce the above
13
      copyright notice, this list of conditions and the following
14
      disclaimer in the documentation and/or other materials provided
15
      with the distribution.
16
    * Neither the name of the XMLUnit nor the names
17
      of its contributors may be used to endorse or promote products
18
      derived from this software without specific prior written
19
      permission.
20

21
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
POSSIBILITY OF SUCH DAMAGE.
33

34
******************************************************************
35
*/
36

37
package org.custommonkey.xmlunit;
38

39
import java.util.ArrayList;
40
import java.util.HashMap;
41
import java.util.List;
42

43
import org.w3c.dom.Attr;
44
import org.w3c.dom.CharacterData;
45
import org.w3c.dom.CDATASection;
46
import org.w3c.dom.Comment;
47
import org.w3c.dom.Document;
48
import org.w3c.dom.DocumentType;
49
import org.w3c.dom.Element;
50
import org.w3c.dom.NamedNodeMap;
51
import org.w3c.dom.Node;
52
import org.w3c.dom.NodeList;
53
import org.w3c.dom.ProcessingInstruction;
54
import org.w3c.dom.Text;
55

56
/**
57
 * Class that has responsibility for comparing Nodes and notifying a
58
 * DifferenceListener of any differences or dissimilarities that are found.
59
 * Knows how to compare namespaces and nested child nodes, but currently
60
 * only compares nodes of type ELEMENT_NODE, CDATA_SECTION_NODE,
61
 * COMMENT_NODE, DOCUMENT_TYPE_NODE, PROCESSING_INSTRUCTION_NODE and TEXT_NODE.
62
 * Nodes of other types (eg ENTITY_NODE) will be skipped.
63
 * @see DifferenceListener#differenceFound(Difference)
64
 */
65
public class DifferenceEngine
66
    implements DifferenceConstants, DifferenceEngineContract {
67

68
    /**
69
     * Exception instance used internally to control flow
70
     * when a difference is found
71
     */
72
    private static final DifferenceFoundException flowControlException =
1✔
73
        new DifferenceFoundException();
74

75
    private static final String NULL_NODE = "null";
76
    private static final String NOT_NULL_NODE = "not null";
77
    private static final String ATTRIBUTE_ABSENT = "[attribute absent]";
78
    private final ComparisonController controller;
79
    private MatchTracker matchTracker;
80
    private final XpathNodeTracker controlTracker;
81
    private final XpathNodeTracker testTracker;
82

83
    /**
84
     * Simple constructor that uses no MatchTracker at all.
85
     * @param controller the instance used to determine whether a Difference
86
     * detected by this class should halt further comparison or not
87
     * @see ComparisonController#haltComparison(Difference)
88
     */
89
    public DifferenceEngine(ComparisonController controller) {
90
        this(controller, null);
1✔
91
    }
1✔
92

93
    /**
94
     * Simple constructor
95
     * @param controller the instance used to determine whether a Difference
96
     * detected by this class should halt further comparison or not
97
     * @param matchTracker the instance that is notified on each
98
     * successful match.  May be null.
99
     * @see ComparisonController#haltComparison(Difference)
100
     * @see MatchTracker#matchFound(Difference)
101
     */
102
    public DifferenceEngine(ComparisonController controller,
103
                            MatchTracker matchTracker) {
1✔
104
        this.controller = controller;
1✔
105
        this.matchTracker = matchTracker;
1✔
106
        this.controlTracker = new XpathNodeTracker();
1✔
107
        this.testTracker = new XpathNodeTracker();
1✔
108
    }
1✔
109

110
    /**
111
     * @param matchTracker the instance that is notified on each
112
     * successful match.  May be null.
113
     */
114
    public void setMatchTracker(MatchTracker matchTracker) {
115
        this.matchTracker = matchTracker;
1✔
116
    }
1✔
117

118
    /**
119
     * Entry point for Node comparison testing.
120
     * @param control Control XML to compare
121
     * @param test Test XML to compare
122
     * @param listener Notified of any {@link Difference differences} detected
123
     * during node comparison testing
124
     * @param elementQualifier Used to determine which elements qualify for
125
     * comparison e.g. when a node has repeated child elements that may occur
126
     * in any sequence and that sequence is not considered important.
127
     */
128
    public void compare(Node control, Node test, DifferenceListener listener,
129
                        ElementQualifier elementQualifier) {
130
        controlTracker.reset();
1✔
131
        testTracker.reset();
1✔
132
        try {
133
            compare(getNullOrNotNull(control), getNullOrNotNull(test),
1✔
134
                    control, test, listener, NODE_TYPE);
135
            if (control!=null) {
1!
136
                compareNode(control, test, listener, elementQualifier);
1✔
137
            }
138
        } catch (DifferenceFoundException e) {
1✔
139
            // thrown by the protected compare() method to terminate the
140
            // comparison and unwind the call stack back to here
141
        }
1✔
142
    }
1✔
143

144
    private static String getNullOrNotNull(Node aNode) {
145
        return aNode==null ? NULL_NODE : NOT_NULL_NODE;
1✔
146
    }
147

148
    /**
149
     * First point of call: if nodes are comparable it compares node values then
150
     *  recurses to compare node children.
151
     * @param control control node
152
     * @param test test node
153
     * @param listener difference listener
154
     * @param elementQualifier element qualifier
155
     * @throws DifferenceFoundException if a difference has been found
156
     */
157
    protected void compareNode(Node control, Node test,
158
                               DifferenceListener listener, ElementQualifier elementQualifier)
159
        throws DifferenceFoundException {
160
        boolean comparable = compareNodeBasics(control, test, listener);
1✔
161
        boolean isDocumentNode = false;
1✔
162

163
        if (comparable) {
1✔
164
            switch (control.getNodeType()) {
1!
165
            case Node.ELEMENT_NODE:
166
                compareElement((Element)control, (Element)test, listener);
1✔
167
                break;
1✔
168
            case Node.CDATA_SECTION_NODE:
169
            case Node.TEXT_NODE:
170
                compareText((CharacterData) control,
1✔
171
                            (CharacterData) test, listener);
172
                break;
1✔
173
            case Node.COMMENT_NODE:
174
                compareComment((Comment)control, (Comment)test, listener);
1✔
175
                break;
1✔
176
            case Node.DOCUMENT_TYPE_NODE:
177
                compareDocumentType((DocumentType)control,
×
178
                                    (DocumentType)test, listener);
179
                break;
×
180
            case Node.PROCESSING_INSTRUCTION_NODE:
181
                compareProcessingInstruction((ProcessingInstruction)control,
1✔
182
                                             (ProcessingInstruction)test, listener);
183
                break;
1✔
184
            case Node.DOCUMENT_NODE:
185
                isDocumentNode = true;
1✔
186
                compareDocument((Document)control, (Document) test,
1✔
187
                                listener, elementQualifier);
188
                break;
1✔
189
            default:
190
                listener.skippedComparison(control, test);
×
191
            }
192
        }
193

194
        compareHasChildNodes(control, test, listener);
1✔
195
        if (isDocumentNode) {
1✔
196
            Element controlElement = ((Document)control).getDocumentElement();
1✔
197
            Element testElement = ((Document)test).getDocumentElement();
1✔
198
            if (controlElement!=null && testElement!=null) {
1!
199
                compareNode(controlElement, testElement, listener, elementQualifier);
1✔
200
            }
201
        } else {
1✔
202
            controlTracker.indent();
1✔
203
            testTracker.indent();
1✔
204
            compareNodeChildren(control, test, listener, elementQualifier);
1✔
205
            controlTracker.outdent();
1✔
206
            testTracker.outdent();
1✔
207
        }
208
    }
1✔
209

210
    /**
211
     * Compare two Documents for doctype and then element differences
212
     * @param control control document
213
     * @param test test document
214
     * @param listener difference listener
215
     * @param elementQualifier element qualifier
216
     * @throws DifferenceFoundException if a difference has been found
217
     */
218
    protected void compareDocument(Document control, Document test,
219
                                   DifferenceListener listener, ElementQualifier elementQualifier)
220
        throws DifferenceFoundException {
221
        DocumentType controlDoctype = control.getDoctype();
1✔
222
        DocumentType testDoctype = test.getDoctype();
1✔
223
        compare(getNullOrNotNull(controlDoctype),
1✔
224
                getNullOrNotNull(testDoctype),
1✔
225
                controlDoctype, testDoctype, listener,
226
                HAS_DOCTYPE_DECLARATION);
227
        if (controlDoctype!=null && testDoctype!=null) {
1!
228
            compareNode(controlDoctype, testDoctype, listener, elementQualifier);
×
229
        }
230
    }
1✔
231

232
    /**
233
     * Compares node type and node namespace characteristics: basically
234
     * determines if nodes are comparable further
235
     * @param control control node
236
     * @param test test node
237
     * @param listener difference listener
238
     * @return true if the nodes are comparable further, false otherwise
239
     * @throws DifferenceFoundException if a difference has been found
240
     */
241
    protected boolean compareNodeBasics(Node control, Node test,
242
                                        DifferenceListener listener) throws DifferenceFoundException {
243
        controlTracker.visited(control);
1✔
244
        testTracker.visited(test);
1✔
245

246
        Short controlType = new Short(control.getNodeType());
1✔
247
        Short testType = new Short(test.getNodeType());
1✔
248

249
        boolean textAndCDATA = comparingTextAndCDATA(control.getNodeType(),
1✔
250
                                                     test.getNodeType());
1✔
251
        if (!textAndCDATA) {
1!
252
            compare(controlType, testType, control, test, listener,
1✔
253
                    NODE_TYPE);
254
        }
255
        compare(control.getNamespaceURI(), test.getNamespaceURI(),
1✔
256
                control, test, listener, NAMESPACE_URI);
257
        compare(control.getPrefix(), test.getPrefix(),
1✔
258
                control, test, listener, NAMESPACE_PREFIX);
259

260
        return textAndCDATA || controlType.equals(testType);
1!
261
    }
262

263
    private boolean comparingTextAndCDATA(short controlType, short testType) {
264
        return XMLUnit.getIgnoreDiffBetweenTextAndCDATA() &&
1!
265
            (controlType == Node.TEXT_NODE
266
             && testType == Node.CDATA_SECTION_NODE
267
             ||
268
             testType == Node.TEXT_NODE
269
             && controlType == Node.CDATA_SECTION_NODE);
270
    }
271

272
    /**
273
     * Compare the number of children, and if the same, compare the actual
274
     *  children via their NodeLists.
275
     * @param control control node
276
     * @param test test node
277
     * @param listener difference listener
278
     * @throws DifferenceFoundException if a difference has been found
279
     */
280
    protected void compareHasChildNodes(Node control, Node test,
281
                                        DifferenceListener listener) throws DifferenceFoundException {
282
        Boolean controlHasChildren = hasChildNodes(control);
1✔
283
        Boolean testHasChildren = hasChildNodes(test);
1✔
284
        compare(controlHasChildren, testHasChildren, control, test,
1✔
285
                listener, HAS_CHILD_NODES);
286
    }
1✔
287

288
    /**
289
     * Tests whether a Node has children, taking ignoreComments
290
     * setting into account.
291
     */
292
    private Boolean hasChildNodes(Node n) {
293
        boolean flag = n.hasChildNodes();
1✔
294
        if (flag && XMLUnit.getIgnoreComments()) {
1✔
295
            List nl = nodeList2List(n.getChildNodes());
1✔
296
            flag = !nl.isEmpty();
1✔
297
        }
298
        return flag ? Boolean.TRUE : Boolean.FALSE;
1✔
299
    }
300

301
    /**
302
     * Returns the NodeList's Nodes as List, taking ignoreComments
303
     * into account.
304
     */
305
    static List<Node> nodeList2List(NodeList nl) {
306
        int len = nl.getLength();
1✔
307
        List<Node> l = new ArrayList<Node>(len);
1✔
308
        for (int i = 0; i < len; i++) {
1✔
309
            Node n = nl.item(i);
1✔
310
            if (!XMLUnit.getIgnoreComments() || !(n instanceof Comment)) {
1✔
311
                l.add(n);
1✔
312
            }
313
        }
314
        return l;
1✔
315
    }
316

317
    /**
318
     * Compare the number of children, and if the same, compare the actual
319
     *  children via their NodeLists.
320
     * @param control control node
321
     * @param test test node
322
     * @param listener difference listener
323
     * @param elementQualifier element qualifier
324
     * @throws DifferenceFoundException if a difference has been found
325
     */
326
    protected void compareNodeChildren(Node control, Node test,
327
                                       DifferenceListener listener, ElementQualifier elementQualifier)
328
        throws DifferenceFoundException {
329

330
        List<Node> controlChildren = nodeList2List(control.getChildNodes());
1✔
331
        List<Node> testChildren = nodeList2List(test.getChildNodes());
1✔
332

333
        Integer controlLength = new Integer(controlChildren.size());
1✔
334
        Integer testLength = new Integer(testChildren.size());
1✔
335
        compare(controlLength, testLength, control, test, listener,
1✔
336
                CHILD_NODELIST_LENGTH);
337

338
        if (control.hasChildNodes() || test.hasChildNodes()) {
1!
339
            if (!control.hasChildNodes()) {
1!
340
                for (Node aTestChildren : testChildren) {
×
341
                    missingNode(null, aTestChildren, listener);
×
342
                }
×
343
            } else if (!test.hasChildNodes()) {
1✔
344
                for (Node aControlChildren : controlChildren) {
1✔
345
                    missingNode(aControlChildren, null, listener);
1✔
346
                }
1✔
347
             } else {
348
                compareNodeList(controlChildren, testChildren,
1✔
349
                                controlLength.intValue(), listener, elementQualifier);
1✔
350
            }
351
        }
352
    }
1✔
353

354
    /**
355
     * Compare the contents of two node list one by one, assuming that order
356
     * of children is NOT important: matching begins at same position in test
357
     * list as control list.
358
     * @param controlChildren children of the control node
359
     * @param testChildren children of the test node
360
     * @param numNodes convenience parameter because the calling method should
361
     *  know the value already
362
     * @param listener difference listener
363
     * @param elementQualifier used to determine which of the child elements in
364
     * the test NodeList should be compared to the current child element in the
365
     * control NodeList.
366
     * @throws DifferenceFoundException if a difference has been found
367
     */
368
    protected void compareNodeList(final List<Node> controlChildren,
369
                                   final List<Node> testChildren,
370
                                   final int numNodes,
371
                                   final DifferenceListener listener,
372
                                   final ElementQualifier elementQualifier)
373
        throws DifferenceFoundException {
374

375
        int j;
376
        final int lastTestNode = testChildren.size() - 1;
1✔
377
        testTracker.preloadChildList(testChildren);
1✔
378

379
        HashMap<Node, Node> matchingNodes = new HashMap<Node, Node>();
1✔
380
        HashMap<Node, Integer> matchingNodeIndexes = new HashMap<Node, Integer>();
1✔
381

382
        List<Node> unmatchedTestNodes = new ArrayList<Node>(testChildren);
1✔
383

384
        // first pass to find the matching nodes in control and test docs
385
        for (int i=0; i < numNodes; ++i) {
1✔
386
            Node nextControl = controlChildren.get(i);
1✔
387
            boolean matchOnElement = nextControl instanceof Element;
1✔
388
            short findNodeType = nextControl.getNodeType();
1✔
389
            int startAt = i > lastTestNode ? lastTestNode : i;
1✔
390
            j = startAt;
1✔
391

392
            boolean matchFound = false;
1✔
393

394
            /*
395
             * XMLUnit 1.2 and earlier don't check whether the
396
             * "matched" test node has already been matched to a
397
             * different control node and will happily match the same
398
             * test node to each and every control node, if necessary.
399
             *
400
             * I (Stefan) feel this is wrong but can't change it
401
             * without breaking backwards compatibility
402
             * (testXpathLocation12 in test_DifferenceEngine which
403
             * predates XMLUnit 1.0 fails, so at one point it has been
404
             * the expected and intended behaviour).
405
             *
406
             * As a side effect it may leave test nodes inside the
407
             * unmatched list, see
408
             * https://sourceforge.net/tracker/?func=detail&aid=2807167&group_id=23187&atid=377768
409
             *
410
             * To overcome the later problem the code will now prefer
411
             * test nodes that haven't already been matched to any
412
             * other node and falls back to the first
413
             * (multiply-)matched node if none could be found.  Yes,
414
             * this is strange.
415
             */
416
            int fallbackMatch = -1;
1✔
417

418
            while (!matchFound) {
1✔
419
                Node t = testChildren.get(j);
1✔
420
                if (findNodeType == t.getNodeType()
1✔
421
                    || comparingTextAndCDATA(findNodeType, t.getNodeType())) {
1!
422
                    matchFound = !matchOnElement
1✔
423
                        || elementQualifier == null
424
                        || elementQualifier
425
                        .qualifyForComparison((Element) nextControl,
1✔
426
                                              (Element) t);
427
                }
428
                if (matchFound && !unmatchedTestNodes.contains(t)) {
1✔
429
                    /*
430
                     * test node already matched to a different
431
                     * control node, try the other test nodes first
432
                     * but keep this as "fallback" (unless there
433
                     * already is a fallback)
434
                     */
435
                    if (fallbackMatch < 0) {
1!
436
                        fallbackMatch = j;
1✔
437
                    }
438
                    matchFound = false;
1✔
439
                }
440
                if (!matchFound) {
1✔
441
                    ++j;
1✔
442
                    if (j > lastTestNode) {
1✔
443
                        j = 0;
1✔
444
                    }
445
                    if (j == startAt) {
1✔
446
                        // been through all children
447
                        break;
1✔
448
                    }
449
                }
450
            }
1✔
451
            if (!matchFound && XMLUnit.getCompareUnmatched()
1!
452
                && fallbackMatch >= 0) {
453
                matchFound = true;
1✔
454
                j = fallbackMatch;
1✔
455
            }
456
            if (matchFound) {
1✔
457
                matchingNodes.put(nextControl, testChildren.get(j));
1✔
458
                matchingNodeIndexes.put(nextControl, new Integer(j));
1✔
459
                unmatchedTestNodes.remove(testChildren.get(j));
1✔
460
            }
461
        }
462

463
        // next, do the actual comparision on those that matched - or
464
        // match them against the first test nodes that didn't match
465
        // any other control nodes
466
        for (int i=0; i < numNodes; ++i) {
1✔
467
            Node nextControl = controlChildren.get(i);
1✔
468
            Node nextTest = matchingNodes.get(nextControl);
1✔
469
            Integer testIndex = matchingNodeIndexes.get(nextControl);
1✔
470
            if (nextTest == null && XMLUnit.getCompareUnmatched()
1!
471
                && !unmatchedTestNodes.isEmpty()) {
1✔
472
                nextTest = unmatchedTestNodes.get(0);
1✔
473
                testIndex = new Integer(testChildren.indexOf(nextTest));
1✔
474
                unmatchedTestNodes.remove(0);
1✔
475
            }
476
            if (nextTest != null) {
1✔
477
                compareNode(nextControl, nextTest, listener, elementQualifier);
1✔
478
                compare(new Integer(i), testIndex,
1✔
479
                        nextControl, nextTest, listener,
480
                        CHILD_NODELIST_SEQUENCE);
481
            } else {
482
                missingNode(nextControl, null, listener);
1✔
483
            }
484
        }
485

486
        // now handle remaining unmatched test nodes
487
        for (Node node : unmatchedTestNodes) {
1✔
488
            missingNode(null, node, listener);
1✔
489
        }
1✔
490
    }
1✔
491

492
    private void missingNode(Node control, Node test,
493
                             DifferenceListener listener)
494
        throws DifferenceFoundException {
495
        if (control != null) {
1✔
496
            controlTracker.visited(control);
1✔
497
            compare(getQName(control), null, control, null,
1✔
498
                    listener, CHILD_NODE_NOT_FOUND, controlTracker, null);
499
        } else {
500
            testTracker.visited(test);
1✔
501
            compare(null, getQName(test), null, test, listener,
1✔
502
                    CHILD_NODE_NOT_FOUND, null, testTracker);
503
        }
504
    }
1✔
505

506
    /**
507
     * @param aNode
508
     * @return true if the node has a namespace
509
     */
510
    private static boolean isNamespaced(Node aNode) {
511
        String namespace = aNode.getNamespaceURI();
1✔
512
        return namespace != null && namespace.length() > 0;
1!
513
    }
514

515
    /**
516
     * Compare 2 elements and their attributes
517
     * @param control element
518
     * @param test test element
519
     * @param listener difference listener
520
     * @throws DifferenceFoundException if a difference has been found
521
     */
522
    protected void compareElement(Element control, Element test,
523
                                  DifferenceListener listener) throws DifferenceFoundException {
524
        compare(getUnNamespacedNodeName(control), getUnNamespacedNodeName(test),
1✔
525
                control, test, listener, ELEMENT_TAG_NAME);
526

527
        NamedNodeMap controlAttr = control.getAttributes();
1✔
528
        Integer controlNonXmlnsAttrLength =
1✔
529
            getNonSpecialAttrLength(controlAttr);
1✔
530
        NamedNodeMap testAttr = test.getAttributes();
1✔
531
        Integer testNonXmlnsAttrLength = getNonSpecialAttrLength(testAttr);
1✔
532
        compare(controlNonXmlnsAttrLength, testNonXmlnsAttrLength,
1✔
533
                control, test, listener, ELEMENT_NUM_ATTRIBUTES);
534

535
        compareElementAttributes(control, test, controlAttr, testAttr,
1✔
536
                                 listener);
537
    }
1✔
538

539
    /**
540
     * The number of attributes not related to namespace declarations
541
     * and/or Schema location.
542
     */
543
    private Integer getNonSpecialAttrLength(NamedNodeMap attributes) {
544
        int length = 0, maxLength = attributes.getLength();
1✔
545
        for (int i = 0; i < maxLength; ++i) {
1✔
546
            Attr a = (Attr) attributes.item(i);
1✔
547
            if (!isXMLNSAttribute(a)
1✔
548
                && !isRecognizedXMLSchemaInstanceAttribute(a)) {
1✔
549
                ++length;
1✔
550
            }
551
        }
552
        return new Integer(length);
1✔
553
    }
554

555
    void compareElementAttributes(Element control, Element test,
556
                                  NamedNodeMap controlAttr,
557
                                  NamedNodeMap testAttr,
558
                                  DifferenceListener listener)
559
        throws DifferenceFoundException {
560
        ArrayList<Attr> unmatchedTestAttrs = new ArrayList<Attr>();
1✔
561
        for (int i=0; i < testAttr.getLength(); ++i) {
1✔
562
            Attr nextAttr = (Attr) testAttr.item(i);
1✔
563
            if (!isXMLNSAttribute(nextAttr)) {
1✔
564
                unmatchedTestAttrs.add(nextAttr);
1✔
565
            }
566
        }
567

568
        for (int i=0; i < controlAttr.getLength(); ++i) {
1✔
569
            Attr nextAttr = (Attr) controlAttr.item(i);
1✔
570
            // xml namespacing is handled in compareNodeBasics
571
            if (!isXMLNSAttribute(nextAttr)) {
1✔
572
                boolean isNamespacedAttr = isNamespaced(nextAttr);
1✔
573
                String attrName = getUnNamespacedNodeName(nextAttr, isNamespacedAttr);
1✔
574
                Attr compareTo;
575

576
                if (isNamespacedAttr) {
1✔
577
                    compareTo = (Attr) testAttr.getNamedItemNS(
1✔
578
                                                               nextAttr.getNamespaceURI(), attrName);
1✔
579
                } else {
580
                    compareTo = (Attr) testAttr.getNamedItem(attrName);
1✔
581
                }
582

583
                if (compareTo != null) {
1✔
584
                    unmatchedTestAttrs.remove(compareTo);
1✔
585
                }
586

587
                if (isRecognizedXMLSchemaInstanceAttribute(nextAttr)) {
1✔
588
                    compareRecognizedXMLSchemaInstanceAttribute(nextAttr,
1✔
589
                                                                compareTo,
590
                                                                listener);
591

592
                } else if (compareTo != null) {
1✔
593
                    compareAttribute(nextAttr, compareTo, listener);
1✔
594

595
                    if (!XMLUnit.getIgnoreAttributeOrder()) {
1✔
596
                        Attr attributeItem = (Attr) testAttr.item(i);
1✔
597
                        String testAttrName = ATTRIBUTE_ABSENT;
1✔
598
                        if (attributeItem != null) {
1!
599
                            testAttrName =
1✔
600
                                getUnNamespacedNodeName(attributeItem);
1✔
601
                        }
602
                        compare(attrName, testAttrName,
1✔
603
                                nextAttr, compareTo, listener, ATTR_SEQUENCE);
604
                    }
1✔
605
                } else {
606
                    controlTracker.clearTrackedAttribute();
1✔
607
                    controlTracker.visited(nextAttr);
1✔
608
                    testTracker.clearTrackedAttribute();
1✔
609
                    compare(getQName(nextAttr, isNamespacedAttr), null,
×
610
                            control, test, listener,
611
                            ATTR_NAME_NOT_FOUND);
612
                }
613
            }
614
        }
615

616
        for (Attr nextAttr : unmatchedTestAttrs) {
1✔
617
            if (isRecognizedXMLSchemaInstanceAttribute(nextAttr)) {
1!
618
                compareRecognizedXMLSchemaInstanceAttribute(null, nextAttr,
1✔
619
                                                            listener);
620
            } else {
621
                controlTracker.clearTrackedAttribute();
×
622
                testTracker.clearTrackedAttribute();
×
623
                testTracker.visited(nextAttr);
×
624
                compare(null,
×
625
                        getQName(nextAttr),
×
626
                        control, test, listener, ATTR_NAME_NOT_FOUND);
627
            }
628
        }
1✔
629

630
        controlTracker.clearTrackedAttribute();
1✔
631
        testTracker.clearTrackedAttribute();
1✔
632
    }
1✔
633

634
    private String getUnNamespacedNodeName(Node aNode) {
635
        return getUnNamespacedNodeName(aNode, isNamespaced(aNode));
1✔
636
    }
637

638
    private static String getUnNamespacedNodeName(Node aNode, boolean isNamespacedNode) {
639
        if (isNamespacedNode) {
1✔
640
            return aNode.getLocalName();
1✔
641
        }
642
        return aNode.getNodeName();
1✔
643
    }
644

645
    private String getQName(Node aNode) {
646
        return getQName(aNode, isNamespaced(aNode));
1✔
647
    }
648

649
    private static String getQName(Node aNode, boolean isNamespacedNode) {
650
        if (isNamespacedNode) {
1✔
651
            return "{" + aNode.getNamespaceURI() + "}" + aNode.getLocalName();
1✔
652
        }
653
        return aNode.getNodeName();
1✔
654
    }
655

656
    /**
657
     * @param attribute
658
     * @return true if the attribute represents a namespace declaration
659
     */
660
    private boolean isXMLNSAttribute(Attr attribute) {
661
        return XMLConstants.XMLNS_PREFIX.equals(attribute.getPrefix()) ||
1✔
662
            XMLConstants.XMLNS_PREFIX.equals(attribute.getName());
1✔
663
    }
664

665
    /**
666
     * @param attr
667
     * @return true if the attribute is an XML Schema Instance
668
     * namespace attribute XMLUnit treats in a special way.
669
     */
670
    private boolean isRecognizedXMLSchemaInstanceAttribute(Attr attr) {
671
        return XMLConstants
1✔
672
            .W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(attr.getNamespaceURI())
1✔
673
            && (XMLConstants
674
                .W3C_XML_SCHEMA_INSTANCE_SCHEMA_LOCATION_ATTR
675
                .equals(attr.getLocalName())
1✔
676
                || XMLConstants
677
                .W3C_XML_SCHEMA_INSTANCE_NO_NAMESPACE_SCHEMA_LOCATION_ATTR
678
                .equals(attr.getLocalName()));
1!
679
    }
680

681
    /**
682
     * Compare two attributes
683
     * @param control control attribute
684
     * @param test test attribute
685
     * @param listener difference listener
686
     * @throws DifferenceFoundException if a difference has been found
687
     */
688
    protected void compareRecognizedXMLSchemaInstanceAttribute(Attr control,
689
                                                               Attr test,
690
                                                               DifferenceListener listener)
691
        throws DifferenceFoundException {
692
        Attr nonNullNode = control != null ? control : test;
1✔
693
        Difference d =
694
            XMLConstants.W3C_XML_SCHEMA_INSTANCE_SCHEMA_LOCATION_ATTR
695
            .equals(nonNullNode.getLocalName())
1✔
696
            ? SCHEMA_LOCATION : NO_NAMESPACE_SCHEMA_LOCATION;
1✔
697

698
        if (control != null) {
1✔
699
            controlTracker.visited(control);
1✔
700
        }
701
        if (test != null) {
1✔
702
            testTracker.visited(test);
1✔
703
        }
704

705
        compare(control != null ? control.getValue() : ATTRIBUTE_ABSENT,
1✔
706
                test != null ? test.getValue() : ATTRIBUTE_ABSENT,
1✔
707
                control, test, listener, d);
708
    }
1✔
709

710
    /**
711
     * Compare two attributes
712
     * @param control control attribute
713
     * @param test test attribute
714
     * @param listener difference listener
715
     * @throws DifferenceFoundException if a difference has been found
716
     */
717
    protected void compareAttribute(Attr control, Attr test,
718
                                    DifferenceListener listener) throws DifferenceFoundException {
719
        controlTracker.visited(control);
1✔
720
        testTracker.visited(test);
1✔
721

722
        compare(control.getPrefix(), test.getPrefix(), control, test,
1✔
723
                listener, NAMESPACE_PREFIX);
724

725
        compare(control.getValue(), test.getValue(), control, test,
1✔
726
                listener, ATTR_VALUE);
727

728
        compare(control.getSpecified() ? Boolean.TRUE : Boolean.FALSE,
1!
729
                test.getSpecified() ? Boolean.TRUE : Boolean.FALSE,
1✔
730
                control, test, listener, ATTR_VALUE_EXPLICITLY_SPECIFIED);
731
    }
1✔
732

733
    /**
734
     * Compare two CDATA sections - unused, kept for backwards compatibility
735
     * @param control control cdata
736
     * @param test test cdata
737
     * @param listener difference listener
738
     * @throws DifferenceFoundException if a difference has been found
739
     */
740
    protected void compareCDataSection(CDATASection control, CDATASection test,
741
                                       DifferenceListener listener) throws DifferenceFoundException {
742
        compareText(control, test, listener);
×
743
    }
×
744

745
    /**
746
     * Compare two comments
747
     * @param control control comment
748
     * @param test test comment
749
     * @param listener difference listener
750
     * @throws DifferenceFoundException if a difference has been found
751
     */
752
    protected void compareComment(Comment control, Comment test,
753
                                  DifferenceListener listener) throws DifferenceFoundException {
754
        if (!XMLUnit.getIgnoreComments()) {
1!
755
            compareCharacterData(control, test, listener, COMMENT_VALUE);
1✔
756
        }
757
    }
1✔
758

759
    /**
760
     * Compare two DocumentType nodes
761
     * @param control control document type
762
     * @param test test document type
763
     * @param listener difference listener
764
     * @throws DifferenceFoundException if a difference has been found
765
     */
766
    protected void compareDocumentType(DocumentType control, DocumentType test,
767
                                       DifferenceListener listener) throws DifferenceFoundException {
768
        compare(control.getName(), test.getName(), control, test, listener,
1✔
769
                DOCTYPE_NAME);
770
        compare(control.getPublicId(), test.getPublicId(), control, test, listener,
1✔
771
                DOCTYPE_PUBLIC_ID);
772

773
        compare(control.getSystemId(), test.getSystemId(),
1✔
774
                control, test, listener, DOCTYPE_SYSTEM_ID);
775
    }
1✔
776

777
    /**
778
     * Compare two processing instructions
779
     * @param control control processing instruction
780
     * @param test test processing instruction
781
     * @param listener difference listener
782
     * @throws DifferenceFoundException if a difference has been found
783
     */
784
    protected void compareProcessingInstruction(ProcessingInstruction control,
785
                                                ProcessingInstruction test, DifferenceListener listener)
786
        throws DifferenceFoundException {
787
        compare(control.getTarget(), test.getTarget(), control, test, listener,
1✔
788
                PROCESSING_INSTRUCTION_TARGET);
789
        compare(control.getData(), test.getData(), control, test, listener,
1✔
790
                PROCESSING_INSTRUCTION_DATA);
791
    }
1✔
792

793
    /**
794
     * Compare text - unused, kept for backwards compatibility
795
     * @param control control text
796
     * @param test test text
797
     * @param listener difference listener
798
     * @throws DifferenceFoundException if a difference has been found
799
     */
800
    protected void compareText(Text control, Text test,
801
                               DifferenceListener listener)
802
        throws DifferenceFoundException {
803
        compareText((CharacterData) control, (CharacterData) test, listener);
×
804
    }
×
805

806
    /**
807
     * Compare text
808
     * @param control control text
809
     * @param test test text
810
     * @param listener difference listener
811
     * @throws DifferenceFoundException if a difference has been found
812
     */
813
    protected void compareText(CharacterData control, CharacterData test,
814
                               DifferenceListener listener)
815
        throws DifferenceFoundException {
816
        compareCharacterData(control, test, listener,
1✔
817
                             control instanceof CDATASection ? CDATA_VALUE : TEXT_VALUE);
1✔
818
    }
1✔
819

820
    /**
821
     * Character comparison method used by comments, text and CDATA sections
822
     * @param control control text
823
     * @param test test text
824
     * @param listener difference listener
825
     * @param difference the difference type
826
     * @throws DifferenceFoundException if a difference has been found
827
     */
828
    private void compareCharacterData(CharacterData control, CharacterData test,
829
                                      DifferenceListener listener, Difference difference)
830
        throws DifferenceFoundException {
831
        compare(control.getData(), test.getData(), control, test, listener,
1✔
832
                difference);
833
    }
1✔
834

835
    /**
836
     * If the expected and actual values are unequal then inform the listener of
837
     *  a difference and throw a DifferenceFoundException.
838
     * @param expected expected value
839
     * @param actual value
840
     * @param control control node
841
     * @param test test node
842
     * @param listener difference listener
843
     * @param difference difference type
844
     * @throws DifferenceFoundException if a difference has been found
845
     */
846
    protected void compare(Object expected, Object actual,
847
                           Node control, Node test, DifferenceListener listener, Difference difference)
848
        throws DifferenceFoundException {
849
        compare(expected, actual, control, test, listener, difference,
1✔
850
                controlTracker, testTracker);
851
    }
1✔
852

853
    /**
854
     * If the expected and actual values are unequal then inform the listener of
855
     *  a difference and throw a DifferenceFoundException.
856
     * @param expected expected value
857
     * @param actual value
858
     * @param control control node
859
     * @param controlLoc XPath location of control node
860
     * @param test test node
861
     * @param testLoc XPath location of test node
862
     * @param listener difference listener
863
     * @param difference difference type
864
     * @throws DifferenceFoundException if a difference has been found
865
     */
866
    protected void compare(Object expected, Object actual,
867
                           Node control, Node test, DifferenceListener listener,
868
                           Difference difference, XpathNodeTracker controlLoc,
869
                           XpathNodeTracker testLoc)
870
        throws DifferenceFoundException {
871
        NodeDetail controlDetail = new NodeDetail(String.valueOf(expected),
1✔
872
                                                  control,
873
                                                  controlLoc == null ? null
1✔
874
                                                  : controlLoc.toXpathString());
1✔
875
        NodeDetail testDetail = new NodeDetail(String.valueOf(actual),
1✔
876
                                               test,
877
                                               testLoc == null ? null
1✔
878
                                               : testLoc.toXpathString());
1✔
879
        Difference differenceInstance = new Difference(difference,
1✔
880
                                                       controlDetail, testDetail);
881
        if (unequal(expected, actual)) {
1✔
882
            listener.differenceFound(differenceInstance);
1✔
883
            if (controller.haltComparison(differenceInstance)) {
1✔
884
                throw flowControlException;
1✔
885
            }
886
        } else if (matchTracker != null) {
1✔
887
            matchTracker.matchFound(differenceInstance);
1✔
888
        }
889
    }
1✔
890

891
    /**
892
     * Test two possibly null values for inequality
893
     * @param expected
894
     * @param actual
895
     * @return TRUE if the values are neither both null, nor equals() equal
896
     */
897
    private boolean unequal(Object expected, Object actual) {
898
        return expected==null ? actual!=null : unequalNotNull(expected, actual);
1✔
899
    }
900

901
    /**
902
     * Test two non-null values for inequality
903
     * @param expected
904
     * @param actual
905
     * @return TRUE if the values are not equals() equal (taking whitespace
906
     *  into account if necessary)
907
     */
908
    private boolean unequalNotNull(Object expected, Object actual) {
909
        if ((XMLUnit.getIgnoreWhitespace() || XMLUnit.getNormalizeWhitespace())
1!
910
            && expected instanceof String && actual instanceof String) {
911
            String expectedString = ((String) expected).trim();
×
912
            String actualString = ((String) actual).trim();
×
913
            if (XMLUnit.getNormalizeWhitespace()) {
×
914
                expectedString = normalizeWhitespace(expectedString);
×
915
                actualString = normalizeWhitespace(actualString);
×
916
            }
917
            return !expectedString.equals(actualString);
×
918
        }
919
        return !(expected.equals(actual));
1✔
920
    }
921

922
    /**
923
     * Replace all whitespace characters with SPACE and collapse
924
     * consecutive whitespace chars to a single SPACE.
925
     */
926
    final static String normalizeWhitespace(String orig) {
927
        StringBuilder sb = new StringBuilder();
1✔
928
        boolean lastCharWasWhitespace = false;
1✔
929
        boolean changed = false;
1✔
930
        char[] characters = orig.toCharArray();
1✔
931
        for (int i = 0; i < characters.length; i++) {
1✔
932
            if (Character.isWhitespace(characters[i])) {
1✔
933
                if (lastCharWasWhitespace) {
1✔
934
                    // suppress character
935
                    changed = true;
1✔
936
                } else {
937
                    sb.append(' ');
1✔
938
                    changed |= characters[i] != ' ';
1✔
939
                    lastCharWasWhitespace = true;
1✔
940
                }
941
            } else {
942
                sb.append(characters[i]);
1✔
943
                lastCharWasWhitespace = false;
1✔
944
            }
945
        }
946
        return changed ? sb.toString() : orig;
1!
947
    }
948

949
    /**
950
     * Marker exception thrown by the protected compare() method and passed
951
     * upwards through the call stack to the public compare() method.
952
     */
953
    protected static final class DifferenceFoundException extends Exception {
954
        private DifferenceFoundException() {
955
            super("This exception is used to control flow");
1✔
956
        }
1✔
957
    }
958

959
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc