• 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

97.38
/xmlunit-core/src/main/java/org/xmlunit/diff/DOMDifferenceEngine.java
1
/*
2
  This file is licensed to You under the Apache License, Version 2.0
3
  (the "License"); you may not use this file except in compliance with
4
  the License.  You may obtain a copy of the License at
5

6
  http://www.apache.org/licenses/LICENSE-2.0
7

8
  Unless required by applicable law or agreed to in writing, software
9
  distributed under the License is distributed on an "AS IS" BASIS,
10
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
  See the License for the specific language governing permissions and
12
  limitations under the License.
13
*/
14

15
package org.xmlunit.diff;
16

17
import java.util.Collections;
18
import java.util.HashMap;
19
import java.util.HashSet;
20
import java.util.LinkedList;
21
import java.util.List;
22
import java.util.Map;
23
import java.util.Set;
24
import javax.xml.XMLConstants;
25
import javax.xml.namespace.QName;
26
import javax.xml.parsers.DocumentBuilderFactory;
27
import javax.xml.transform.Source;
28
import org.xmlunit.XMLUnitException;
29
import org.xmlunit.util.Convert;
30
import org.xmlunit.util.DocumentBuilderFactoryConfigurer;
31
import org.xmlunit.util.IterableNodeList;
32
import org.xmlunit.util.Linqy;
33
import org.xmlunit.util.Mapper;
34
import org.xmlunit.util.Nodes;
35
import org.w3c.dom.Attr;
36
import org.w3c.dom.CharacterData;
37
import org.w3c.dom.Document;
38
import org.w3c.dom.DocumentType;
39
import org.w3c.dom.Element;
40
import org.w3c.dom.NamedNodeMap;
41
import org.w3c.dom.Node;
42
import org.w3c.dom.ProcessingInstruction;
43

44
/**
45
 * Difference engine based on DOM.
46
 */
47
public final class DOMDifferenceEngine extends AbstractDifferenceEngine {
48

49
    /**
50
     * Maps Nodes to their QNames.
51
     */
52
    private static final Mapper<Node, QName> QNAME_MAPPER =
1✔
53
        new Mapper<Node, QName>() {
1✔
54
            @Override
55
            public QName apply(Node n) { return Nodes.getQName(n); }
1✔
56
        };
57

58
    private DocumentBuilderFactory documentBuilderFactory;
59

60
    /**
61
     * Creates a new DOMDifferenceEngine using the default {@link DocumentBuilderFactory}.
62
     */
63
    public DOMDifferenceEngine() {
64
        this(DocumentBuilderFactoryConfigurer.Default.configure(DocumentBuilderFactory.newInstance()));
1✔
65
    }
1✔
66

67
    /**
68
     * Creates a new DOMDifferenceEngine.
69
     *
70
     * <p>The {@link DocumentBuilderFactory} is only used if the
71
     * {@code Source} passed to {@link #compare} is not already a
72
     * {@link javax.xml.transform.dom.DOMSource}.</p>
73
     *
74
     * @param f {@code DocumentBuilderFactory} to use when creating a
75
     * {@link Document} from the {@link Source}s to compare.
76
     *
77
     * @since XMLUnit 2.7.0
78
     */
79
    public DOMDifferenceEngine(final DocumentBuilderFactory f) {
1✔
80
        if (f == null) {
1✔
81
            throw new IllegalArgumentException("factory must not be null");
1✔
82
        }
83
        documentBuilderFactory = f;
1✔
84
    }
1✔
85

86
    /**
87
     * Sets the {@link DocumentBuilderFactory} to use when creating a
88
     * {@link Document} from the {@link Source}s to compare.
89
     *
90
     * <p>This is only used if the {@code Source} passed to {@link #compare}
91
     * is not already a {@link javax.xml.transform.dom.DOMSource}.</p>
92
     *
93
     * @param f {@code DocumentBuilderFactory} to use when creating a
94
     * {@link Document} from the {@link Source}s to compare.
95
     *
96
     * @since XMLUnit 2.2.0
97
     * @deprecated use the one-arg constructor instead
98
     */
99
    public void setDocumentBuilderFactory(DocumentBuilderFactory f) {
100
        if (f == null) {
1!
101
            throw new IllegalArgumentException("factory must not be null");
1✔
102
        }
103
        documentBuilderFactory = f;
×
104
    }
×
105

106
    @Override
107
    public void compare(Source control, Source test) {
108
        if (control == null) {
1!
109
            throw new IllegalArgumentException("control must not be null");
×
110
        }
111
        if (test == null) {
1!
112
            throw new IllegalArgumentException("test must not be null");
×
113
        }
114
        try {
115
            Node controlNode = Convert.toNode(control, documentBuilderFactory);
1✔
116
            Node testNode = Convert.toNode(test, documentBuilderFactory);
1✔
117
            compareNodes(controlNode, xpathContextFor(controlNode),
1✔
118
                         testNode, xpathContextFor(testNode));
1✔
119
        } catch (Exception ex) {
1✔
120
            throw new XMLUnitException("Caught exception during comparison",
1✔
121
                                       ex);
122
        }
1✔
123
    }
1✔
124

125
    private XPathContext xpathContextFor(Node n) {
126
        return new XPathContext(getNamespaceContext(), n);
1✔
127
    }
128

129
    /**
130
     * Recursively compares two XML nodes.
131
     *
132
     * <p>Performs comparisons common to all node types, then performs
133
     * the node type specific comparisons and finally recurses into
134
     * the node's child lists.</p>
135
     *
136
     * <p>Stops as soon as any comparison returns
137
     * ComparisonResult.CRITICAL.</p>
138
     *
139
     * <p>package private to support tests.</p>
140
     */
141
    ComparisonState compareNodes(final Node control, final XPathContext controlContext,
142
                                 final Node test, final XPathContext testContext) {
143
        final Iterable<Node> allControlChildren =
1✔
144
            new IterableNodeList(control.getChildNodes());
1✔
145
        final Iterable<Node> controlChildren =
1✔
146
            Linqy.filter(allControlChildren, getNodeFilter());
1✔
147
        final Iterable<Node> allTestChildren =
1✔
148
            new IterableNodeList(test.getChildNodes());
1✔
149
        final Iterable<Node> testChildren =
1✔
150
            Linqy.filter(allTestChildren, getNodeFilter());
1✔
151
        return compare(new Comparison(ComparisonType.NODE_TYPE,
1✔
152
                                      controlContext, control, control.getNodeType(),
1✔
153
                                      testContext, test, test.getNodeType()))
1✔
154
            .andThen(new Comparison(ComparisonType.NAMESPACE_URI,
1✔
155
                                    controlContext, control, control.getNamespaceURI(),
1✔
156
                                    testContext, test, test.getNamespaceURI()))
1✔
157
            .andThen(new Comparison(ComparisonType.NAMESPACE_PREFIX,
1✔
158
                                    controlContext, control, control.getPrefix(),
1✔
159
                                    testContext, test, test.getPrefix()))
1✔
160
            .andIfTrueThen(control.getNodeType() != Node.ATTRIBUTE_NODE,
1✔
161
                           new Comparison(ComparisonType.CHILD_NODELIST_LENGTH,
162
                                          controlContext, control,
163
                                          Linqy.count(controlChildren),
1✔
164
                                          testContext, test, Linqy.count(testChildren)))
1✔
165
            .andThen(new DeferredComparison() {
1✔
166
                    @Override
167
                    public ComparisonState apply() {
168
                        return nodeTypeSpecificComparison(control, controlContext,
1✔
169
                                                          test, testContext);
170
                    }
171
                })
172
            // and finally recurse into children
173
            .andIfTrueThen(control.getNodeType() != Node.ATTRIBUTE_NODE,
1✔
174
                           compareChildren(controlContext,
1✔
175
                                           allControlChildren,
176
                                           controlChildren,
177
                                           testContext,
178
                                           allTestChildren,
179
                                           testChildren));
180
    }
181

182
    /**
183
     * Dispatches to the node type specific comparison if one is
184
     * defined for the given combination of nodes.
185
     */
186
    private ComparisonState nodeTypeSpecificComparison(Node control,
187
                                                       XPathContext controlContext,
188
                                                       Node test,
189
                                                       XPathContext testContext) {
190
        switch (control.getNodeType()) {
1!
191
        case Node.CDATA_SECTION_NODE:
192
        case Node.COMMENT_NODE:
193
        case Node.TEXT_NODE:
194
            if (test instanceof CharacterData) {
1!
195
                return compareCharacterData((CharacterData) control,
1✔
196
                                            controlContext,
197
                                            (CharacterData) test, testContext);
198
            }
199
            break;
200
        case Node.DOCUMENT_NODE:
201
            if (test instanceof Document) {
1!
202
                return compareDocuments((Document) control, controlContext,
1✔
203
                                        (Document) test, testContext);
204
            }
205
            break;
206
        case Node.ELEMENT_NODE:
207
            if (test instanceof Element) {
1!
208
                return compareElements((Element) control, controlContext,
1✔
209
                                       (Element) test, testContext);
210
            }
211
            break;
212
        case Node.PROCESSING_INSTRUCTION_NODE:
213
            if (test instanceof ProcessingInstruction) {
1!
214
                return
1✔
215
                    compareProcessingInstructions((ProcessingInstruction) control,
1✔
216
                                                  controlContext,
217
                                                  (ProcessingInstruction) test,
218
                                                  testContext);
219
            }
220
            break;
221
        case Node.DOCUMENT_TYPE_NODE:
222
            if (test instanceof DocumentType) {
1!
223
                return compareDocTypes((DocumentType) control, controlContext,
1✔
224
                                       (DocumentType) test, testContext);
225
            }
226
            break;
227
        case Node.ATTRIBUTE_NODE:
228
            if (test instanceof Attr) {
1!
229
                return compareAttributes((Attr) control, controlContext,
1✔
230
                                         (Attr) test, testContext);
231
            }
232
            break;
233
        default:
234
            break;
235
        }
236
        return new OngoingComparisonState();
×
237
    }
238

239
    private DeferredComparison compareChildren(final XPathContext controlContext,
240
                                               final Iterable<Node> allControlChildren,
241
                                               final Iterable<Node> controlChildren,
242
                                               final XPathContext testContext,
243
                                               final Iterable<Node> allTestChildren,
244
                                               final Iterable<Node> testChildren) {
245
        return new DeferredComparison() {
1✔
246
            @Override
247
            public ComparisonState apply() {
248
                controlContext
1✔
249
                    .setChildren(Linqy.map(allControlChildren, ElementSelectors.TO_NODE_INFO));
1✔
250
                testContext
1✔
251
                    .setChildren(Linqy.map(allTestChildren, ElementSelectors.TO_NODE_INFO));
1✔
252
                return compareNodeLists(allControlChildren, controlChildren, controlContext,
1✔
253
                                        allTestChildren, testChildren, testContext);
254
            }
255
        };
256
    }
257

258
    /**
259
     * Compares textual content.
260
     */
261
    private ComparisonState compareCharacterData(CharacterData control,
262
                                                 XPathContext controlContext,
263
                                                 CharacterData test,
264
                                                 XPathContext testContext) {
265
        return compare(new Comparison(ComparisonType.TEXT_VALUE,
1✔
266
                                      controlContext, control, control.getData(),
1✔
267
                                      testContext, test, test.getData()));
1✔
268
    }
269

270
    /**
271
     * Compares document node, doctype and XML declaration properties
272
     */
273
    private ComparisonState compareDocuments(final Document control,
274
                                             final XPathContext controlContext,
275
                                             final Document test,
276
                                             final XPathContext testContext) {
277
        final DocumentType controlDt = filterNode(control.getDoctype());
1✔
278
        final DocumentType testDt = filterNode(test.getDoctype());
1✔
279

280
        return compare(new Comparison(ComparisonType.HAS_DOCTYPE_DECLARATION,
1✔
281
                                      controlContext, control,
282
                                      Boolean.valueOf(controlDt != null),
1✔
283
                                      testContext, test, Boolean.valueOf(testDt != null)))
1✔
284
            .andIfTrueThen(controlDt != null && testDt != null,
1✔
285
                           new DeferredComparison() {
1✔
286
                               @Override
287
                               public ComparisonState apply() {
288
                                   return compareNodes(controlDt, controlContext,
1✔
289
                                                       testDt, testContext);
290
                               }
291
                           })
292
            .andThen(compareDeclarations(control, controlContext,
1✔
293
                                         test, testContext));
294
    }
295

296
    private <T extends Node> T filterNode(T n) {
297
        return n != null && getNodeFilter().test(n) ? n : null;
1✔
298
    }
299

300
    /**
301
     * Compares properties of the doctype declaration.
302
     */
303
    private ComparisonState
304
        compareDocTypes(DocumentType control,
305
                        XPathContext controlContext,
306
                        DocumentType test,
307
                        XPathContext testContext) {
308
        return compare(new Comparison(ComparisonType.DOCTYPE_NAME,
1✔
309
                                      controlContext, control, control.getName(),
1✔
310
                                      testContext, test, test.getName()))
1✔
311
            .andThen(new Comparison(ComparisonType.DOCTYPE_PUBLIC_ID,
1✔
312
                                    controlContext, control, control.getPublicId(),
1✔
313
                                    testContext, test, test.getPublicId()))
1✔
314
            .andThen(new Comparison(ComparisonType.DOCTYPE_SYSTEM_ID,
1✔
315
                                    control, null, control.getSystemId(), null,
1✔
316
                                    test, null, test.getSystemId(), null));
1✔
317
    }
318

319
    /**
320
     * Compares properties of XML declaration.
321
     */
322
    private DeferredComparison compareDeclarations(final Document control,
323
                                                   final XPathContext controlContext,
324
                                                   final Document test,
325
                                                   final XPathContext testContext) {
326
        return new DeferredComparison() {
1✔
327
            @Override
328
            public ComparisonState apply() {
329
                return
1✔
330
                    compare(new Comparison(ComparisonType.XML_VERSION,
1✔
331
                                           controlContext, control, control.getXmlVersion(),
1✔
332
                                           testContext, test, test.getXmlVersion()))
1✔
333
                    .andThen(new Comparison(ComparisonType.XML_STANDALONE,
1✔
334
                                            controlContext, control, control.getXmlStandalone(),
1✔
335
                                            testContext, test, test.getXmlStandalone()))
1✔
336
                    .andThen(new Comparison(ComparisonType.XML_ENCODING,
1✔
337
                                            controlContext, control, control.getXmlEncoding(),
1✔
338
                                            testContext, test, test.getXmlEncoding()));
1✔
339
            }
340
        };
341
    }
342

343
    /**
344
     * Compares elements node properties, in particular the element's
345
     * name and its attributes.
346
     */
347
    private ComparisonState compareElements(final Element control,
348
                                            final XPathContext controlContext,
349
                                            final Element test,
350
                                            final XPathContext testContext) {
351
        return
1✔
352
            compare(new Comparison(ComparisonType.ELEMENT_TAG_NAME,
1✔
353
                                   controlContext, control, Nodes.getQName(control).getLocalPart(),
1✔
354
                                   testContext, test, Nodes.getQName(test).getLocalPart()))
1✔
355
            .andThen(new DeferredComparison() {
1✔
356
                    @Override
357
                    public ComparisonState apply() {
358
                        return compareElementAttributes(control, controlContext,
1✔
359
                                                        test, testContext);
360
                    }
361
                });
362
    }
363

364
    /**
365
     * Compares element's attributes.
366
     */
367
    private ComparisonState compareElementAttributes(final Element control,
368
                                                     final XPathContext controlContext,
369
                                                     final Element test,
370
                                                     final XPathContext testContext) {
371
        final Attributes controlAttributes = splitAttributes(control.getAttributes());
1✔
372
        controlContext
1✔
373
            .addAttributes(Linqy.map(controlAttributes.remainingAttributes,
1✔
374
                                     QNAME_MAPPER));
375
        final Attributes testAttributes = splitAttributes(test.getAttributes());
1✔
376
        testContext
1✔
377
            .addAttributes(Linqy.map(testAttributes.remainingAttributes,
1✔
378
                                     QNAME_MAPPER));
379

380
        return compare(new Comparison(ComparisonType.ELEMENT_NUM_ATTRIBUTES,
1✔
381
                                      controlContext, control,
382
                                      controlAttributes.remainingAttributes.size(),
1✔
383
                                      testContext, test,
384
                                      testAttributes.remainingAttributes.size()))
1✔
385
            .andThen(new DeferredComparison() {
1✔
386
                    @Override
387
                    public ComparisonState apply() {
388
                        return compareXsiType(controlAttributes.type, controlContext,
1✔
389
                                              testAttributes.type, testContext);
1✔
390
                    }
391
                })
392
            .andThen(new Comparison(ComparisonType.SCHEMA_LOCATION,
1✔
393
                                    controlContext, control,
394
                                    controlAttributes.schemaLocation != null
1✔
395
                                    ? controlAttributes.schemaLocation.getValue() : null,
1✔
396
                                    testContext, test,
397
                                    testAttributes.schemaLocation != null
1✔
398
                                    ? testAttributes.schemaLocation.getValue() : null))
1✔
399
//
400
            .andThen(new Comparison(ComparisonType.NO_NAMESPACE_SCHEMA_LOCATION,
1✔
401
                                    controlContext, control,
402
                                    controlAttributes.noNamespaceSchemaLocation != null ?
1✔
403
                                    controlAttributes.noNamespaceSchemaLocation.getValue()
1✔
404
                                    : null,
1✔
405
                                    testContext, test,
406
                                    testAttributes.noNamespaceSchemaLocation != null
1✔
407
                                    ? testAttributes.noNamespaceSchemaLocation.getValue()
1✔
408
                                    : null))
1✔
409
            .andThen(new NormalAttributeComparer(control, controlContext,
1✔
410
                                                 controlAttributes, test,
411
                                                 testContext, testAttributes));
412
    }
413

414
    private class NormalAttributeComparer implements DeferredComparison {
415
        private final Set<Attr> foundTestAttributes = new HashSet<Attr>();
1✔
416
        private final Element control, test;
417
        private final XPathContext controlContext, testContext;
418
        private final Attributes controlAttributes, testAttributes;
419

420
        private NormalAttributeComparer(Element control,
421
                                        XPathContext controlContext,
422
                                        Attributes controlAttributes,
423
                                        Element test,
424
                                        XPathContext testContext,
425
                                        Attributes testAttributes) {
1✔
426
            this.control = control;
1✔
427
            this.controlContext = controlContext;
1✔
428
            this.controlAttributes = controlAttributes;
1✔
429
            this.test = test;
1✔
430
            this.testContext = testContext;
1✔
431
            this.testAttributes = testAttributes;
1✔
432
        }
1✔
433

434
        @Override
435
        public ComparisonState apply() {
436
            ComparisonState chain = new OngoingComparisonState();
1✔
437
            for (final Attr controlAttr : controlAttributes.remainingAttributes) {
1✔
438
                final QName controlAttrName = Nodes.getQName(controlAttr);
1✔
439
                final Attr testAttr =
1✔
440
                    findMatchingAttr(testAttributes.remainingAttributes,
1✔
441
                                     controlAttr);
442
                final QName testAttrName = testAttr != null
1✔
443
                    ? Nodes.getQName(testAttr) : null;
1✔
444

445
                controlContext.navigateToAttribute(controlAttrName);
1✔
446
                try {
447
                    chain = chain.andThen(
1✔
448
                        new Comparison(ComparisonType.ATTR_NAME_LOOKUP,
449
                                                controlContext, control, controlAttrName,
450
                                                testContext, test, testAttrName));
451

452
                    if (testAttr != null) {
1✔
453
                        testContext.navigateToAttribute(testAttrName);
1✔
454
                        try {
455
                            chain = chain.andThen(new DeferredComparison() {
1✔
456
                                    @Override
457
                                    public ComparisonState apply() {
458
                                        return compareNodes(controlAttr, controlContext,
1✔
459
                                                            testAttr, testContext);
1✔
460
                                    }
461
                                });
462
                            foundTestAttributes.add(testAttr);
1✔
463
                        } finally {
464
                            testContext.navigateToParent();
1✔
465
                        }
466
                    }
467
                } finally {
468
                    controlContext.navigateToParent();
1✔
469
                }
470
            }
1✔
471
            return chain.andThen(new ControlAttributePresentComparer(control,
1✔
472
                                                                     controlContext,
473
                                                                     test, testContext,
474
                                                                     testAttributes,
475
                                                                     foundTestAttributes));
476
        }
477
    }
478

479
    private class ControlAttributePresentComparer implements DeferredComparison {
480

481
        private final Set<Attr> foundTestAttributes;
482
        private final Element control, test;
483
        private final XPathContext controlContext, testContext;
484
        private final Attributes testAttributes;
485

486
        private ControlAttributePresentComparer(Element control,
487
                                                XPathContext controlContext,
488
                                                Element test,
489
                                                XPathContext testContext,
490
                                                Attributes testAttributes,
491
                                                Set<Attr> foundTestAttributes) {
1✔
492
            this.control = control;
1✔
493
            this.controlContext = controlContext;
1✔
494
            this.test = test;
1✔
495
            this.testContext = testContext;
1✔
496
            this.testAttributes = testAttributes;
1✔
497
            this.foundTestAttributes = foundTestAttributes;
1✔
498
        }
1✔
499

500
        @Override
501
        public ComparisonState apply() {
502
            ComparisonState chain = new OngoingComparisonState();
1✔
503
            for (Attr testAttr : testAttributes.remainingAttributes) {
1✔
504
                if (!foundTestAttributes.contains(testAttr)) {
1✔
505
                    QName testAttrName = Nodes.getQName(testAttr);
1✔
506
                    testContext.navigateToAttribute(testAttrName);
1✔
507
                    try {
508
                        chain =
1✔
509
                            chain.andThen(new Comparison(ComparisonType.ATTR_NAME_LOOKUP,
1✔
510
                                                         controlContext, control, null,
511
                                                         testContext, test, testAttrName));
512
                    } finally {
513
                        testContext.navigateToParent();
1✔
514
                    }
515
                }
516
            }
1✔
517
            return chain;
1✔
518
        }
519

520
    }
521

522
    /**
523
     * Compares properties of a processing instruction.
524
     */
525
    private ComparisonState compareProcessingInstructions(ProcessingInstruction control,
526
                                                          XPathContext controlContext,
527
                                                          ProcessingInstruction test,
528
                                                          XPathContext testContext) {
529
        return compare(new Comparison(ComparisonType.PROCESSING_INSTRUCTION_TARGET,
1✔
530
                                      controlContext, control, control.getTarget(),
1✔
531
                                      testContext, test, test.getTarget()))
1✔
532
            .andThen(new Comparison(ComparisonType.PROCESSING_INSTRUCTION_DATA,
1✔
533
                                    controlContext, control, control.getData(),
1✔
534
                                    testContext, test, test.getData()));
1✔
535
    }
536

537
    /**
538
     * Matches nodes of two node lists and invokes compareNode on each pair.
539
     *
540
     * <p>Also performs CHILD_LOOKUP comparisons for each node that
541
     * couldn't be matched to one of the "other" list.</p>
542
     */
543
    private ComparisonState compareNodeLists(Iterable<Node> allControlChildren,
544
                                             Iterable<Node> controlSeq,
545
                                             final XPathContext controlContext,
546
                                             Iterable<Node> allTestChildren,
547
                                             Iterable<Node> testSeq,
548
                                             final XPathContext testContext) {
549
        ComparisonState chain = new OngoingComparisonState();
1✔
550

551
        Iterable<Map.Entry<Node, Node>> matches =
1✔
552
            getNodeMatcher().match(controlSeq, testSeq);
1✔
553
        List<Node> controlList = Linqy.asList(controlSeq);
1✔
554
        List<Node> testList = Linqy.asList(testSeq);
1✔
555

556
        final Map<Node, Integer> controlListForXpathIndex = index(allControlChildren);
1✔
557
        final Map<Node, Integer> testListForXpathIndex = index(allTestChildren);
1✔
558
        final Map<Node, Integer> controlListIndex = index(controlList);
1✔
559
        final Map<Node, Integer> testListIndex = index(testList);
1✔
560

561
        Set<Node> seen = new HashSet<Node>();
1✔
562
        for (Map.Entry<Node, Node> pair : matches) {
1✔
563
            final Node control = pair.getKey();
1✔
564
            seen.add(control);
1✔
565
            final Node test = pair.getValue();
1✔
566
            seen.add(test);
1✔
567
            Integer controlIndexForXpath = controlListForXpathIndex.get(control);
1✔
568
            Integer testIndexForXpath = testListForXpathIndex.get(test);
1✔
569
            Integer controlIndex = controlListIndex.get(control);
1✔
570
            Integer testIndex = testListIndex.get(test);
1✔
571
            if (controlIndexForXpath == null || testIndexForXpath == null
1!
572
                || controlIndex == null || testIndex == null) {
573
                throw new NullPointerException("failed to look up index for pair " + pair);
×
574
            }
575

576
            controlContext.navigateToChild(controlIndexForXpath);
1✔
577
            testContext.navigateToChild(testIndexForXpath);
1✔
578
            try {
579
                chain =
1✔
580
                    chain.andThen(new Comparison(ComparisonType.CHILD_NODELIST_SEQUENCE,
1✔
581
                                                 controlContext, control, Integer.valueOf(controlIndex),
1✔
582
                                                 testContext, test, Integer.valueOf(testIndex)))
1✔
583
                    .andThen(new DeferredComparison() {
1✔
584
                            @Override
585
                            public ComparisonState apply() {
586
                                return compareNodes(control, controlContext,
1✔
587
                                                    test, testContext);
588
                            }
589
                        });
590
            } finally {
591
                testContext.navigateToParent();
1✔
592
                controlContext.navigateToParent();
1✔
593
            }
594
        }
1✔
595

596
        return chain.andThen(new UnmatchedControlNodes(controlListForXpathIndex, controlList,
1✔
597
                controlContext, seen, testContext))
598
            .andThen(new UnmatchedTestNodes(testListForXpathIndex, testList,
1✔
599
                testContext, seen, controlContext));
600
    }
601

602
    private class UnmatchedControlNodes implements DeferredComparison {
603
        private final Map<Node, Integer> controlListForXpathIndex;
604
        private final List<Node> controlList;
605
        private final XPathContext controlContext;
606
        private final Set<Node> seen;
607
        private final XPathContext testContext;
608

609
        private UnmatchedControlNodes(Map<Node, Integer> controlListForXpathIndex,
610
                                      List<Node> controlList,
611
                                      XPathContext controlContext,
612
                                      Set<Node> seen, XPathContext testContext) {
1✔
613
            this.controlListForXpathIndex = controlListForXpathIndex;
1✔
614
            this.controlList = controlList;
1✔
615
            this.controlContext = controlContext;
1✔
616
            this.seen = seen;
1✔
617
            this.testContext = testContext;
1✔
618
        }
1✔
619

620
        @Override
621
        public ComparisonState apply() {
622
            ComparisonState chain = new OngoingComparisonState();
1✔
623
            final int controlSize = controlList.size();
1✔
624
            for (int i = 0; i < controlSize; i++) {
1✔
625
                if (!seen.contains(controlList.get(i))) {
1✔
626
                    controlContext.navigateToChild(controlListForXpathIndex.get(controlList.get(i)));
1✔
627
                    try {
628
                        chain =
1✔
629
                            chain.andThen(new Comparison(ComparisonType.CHILD_LOOKUP,
1✔
630
                                                         controlList.get(i),
1✔
631
                                                         getXPath(controlContext),
1✔
632
                                                         Nodes.getQName(controlList.get(i)), getParentXPath(controlContext),
1✔
633
                                                         null, null, null, getXPath(testContext)));
1✔
634
                    } finally {
635
                        controlContext.navigateToParent();
1✔
636
                    }
637
                }
638
            }
639
            return chain;
1✔
640
        }
641
    }
642

643
    private class UnmatchedTestNodes implements DeferredComparison {
644
        private final Map<Node, Integer> testListForXpathIndex;
645
        private final List<Node> testList;
646
        private final XPathContext testContext;
647
        private final Set<Node> seen;
648
        private final XPathContext controlContext;
649

650
        private UnmatchedTestNodes(Map<Node, Integer> testListForXpathIndex,
651
                                   List<Node> testList, XPathContext testContext,
652
                                   Set<Node> seen, XPathContext controlContext) {
1✔
653
            this.testListForXpathIndex = testListForXpathIndex;
1✔
654
            this.testList = testList;
1✔
655
            this.testContext = testContext;
1✔
656
            this.seen = seen;
1✔
657
            this.controlContext = controlContext;
1✔
658
        }
1✔
659

660
        @Override
661
        public ComparisonState apply() {
662
            ComparisonState chain = new OngoingComparisonState();
1✔
663
            final int testSize = testList.size();
1✔
664
            for (int i = 0; i < testSize; i++) {
1✔
665
                if (!seen.contains(testList.get(i))) {
1✔
666
                    testContext.navigateToChild(testListForXpathIndex.get(testList.get(i)));
1✔
667
                    try {
668
                        chain =
1✔
669
                            chain.andThen(new Comparison(ComparisonType.CHILD_LOOKUP,
1✔
670
                                                         null, null, null, getXPath(controlContext),
1✔
671
                                                         testList.get(i),
1✔
672
                                                         getXPath(testContext),
1✔
673
                                                         Nodes.getQName(testList.get(i)), getParentXPath(testContext)));
1✔
674
                    } finally {
675
                        testContext.navigateToParent();
1✔
676
                    }
677
                }
678
            }
679
            return chain;
1✔
680
        }
681
    }
682

683
    /**
684
     * Compares xsi:type attribute values
685
     */
686
    private ComparisonState compareXsiType(Attr controlAttr,
687
                                           XPathContext controlContext,
688
                                           Attr testAttr,
689
                                           XPathContext testContext) {
690
        boolean mustChangeControlContext = controlAttr != null;
1✔
691
        boolean mustChangeTestContext = testAttr != null;
1✔
692
        if (!mustChangeControlContext && !mustChangeTestContext) {
1!
693
            return new OngoingComparisonState();
1✔
694
        }
695
        boolean attributePresentOnBothSides = mustChangeControlContext
1!
696
            && mustChangeTestContext;
697

698
        try {
699
            QName controlAttrName = null;
1✔
700
            if (mustChangeControlContext) {
1!
701
                controlAttrName = Nodes.getQName(controlAttr);
1✔
702
                controlContext.addAttribute(controlAttrName);
1✔
703
                controlContext.navigateToAttribute(controlAttrName);
1✔
704
            }
705
            QName testAttrName = null;
1✔
706
            if (mustChangeTestContext) {
1✔
707
                testAttrName = Nodes.getQName(testAttr);
1✔
708
                testContext.addAttribute(testAttrName);
1✔
709
                testContext.navigateToAttribute(testAttrName);
1✔
710
            }
711
            return
1✔
712
                compare(new Comparison(ComparisonType.ATTR_NAME_LOOKUP,
1✔
713
                                       controlContext, controlAttr, controlAttrName,
714
                                       testContext, testAttr, testAttrName))
715
                .andIfTrueThen(attributePresentOnBothSides,
1✔
716
                               compareAttributeExplicitness(controlAttr, controlContext,
1✔
717
                                                            testAttr, testContext))
718
                .andIfTrueThen(attributePresentOnBothSides,
1✔
719
                               new Comparison(ComparisonType.ATTR_VALUE,
720
                                              controlContext, controlAttr, valueAsQName(controlAttr),
1✔
721
                                              testContext, testAttr, valueAsQName(testAttr)));
1✔
722
        } finally {
723
            if (mustChangeControlContext) {
1!
724
                controlContext.navigateToParent();
1✔
725
            }
726
            if (mustChangeTestContext) {
1✔
727
                testContext.navigateToParent();
1✔
728
            }
729
        }
730
    }
731

732
    /**
733
     * Compares properties of an attribute.
734
     */
735
    private ComparisonState compareAttributes(Attr control,
736
                                              XPathContext controlContext,
737
                                              Attr test,
738
                                              XPathContext testContext) {
739
        return compareAttributeExplicitness(control, controlContext, test,
1✔
740
                                            testContext).apply()
1✔
741
            .andThen(new Comparison(ComparisonType.ATTR_VALUE,
1✔
742
                                    controlContext, control, control.getValue(),
1✔
743
                                    testContext, test, test.getValue()));
1✔
744
    }
745

746
    /**
747
     * Compares whether two attributes are specified explicitly.
748
     */
749
    private DeferredComparison compareAttributeExplicitness(final Attr control,
750
                                                            final XPathContext controlContext,
751
                                                            final Attr test,
752
                                                            final XPathContext testContext) {
753
        return new DeferredComparison() {
1✔
754
            @Override
755
            public ComparisonState apply() {
756
                return compare(new Comparison(ComparisonType.ATTR_VALUE_EXPLICITLY_SPECIFIED,
1✔
757
                                              controlContext, control, control.getSpecified(),
1✔
758
                                              testContext, test, test.getSpecified()));
1✔
759
            }
760
        };
761
    }
762

763
    /**
764
     * Separates XML namespace related attributes from "normal" attributes.xb
765
     */
766
    private Attributes splitAttributes(final NamedNodeMap map) {
767
        Attr sLoc = (Attr) map.getNamedItemNS(XMLConstants
1✔
768
                                              .W3C_XML_SCHEMA_INSTANCE_NS_URI,
769
                                              "schemaLocation");
770
        Attr nNsLoc = (Attr) map.getNamedItemNS(XMLConstants
1✔
771
                                                .W3C_XML_SCHEMA_INSTANCE_NS_URI,
772
                                                "noNamespaceSchemaLocation");
773
        Attr type = (Attr) map.getNamedItemNS(XMLConstants
1✔
774
                                                .W3C_XML_SCHEMA_INSTANCE_NS_URI,
775
                                                "type");
776
        List<Attr> rest = new LinkedList<Attr>();
1✔
777
        final int len = map.getLength();
1✔
778
        for (int i = 0; i < len; i++) {
1✔
779
            Attr a = (Attr) map.item(i);
1✔
780
            if (!XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(a.getNamespaceURI())
1✔
781
                && a != sLoc && a != nNsLoc && a != type
782
                && getAttributeFilter().test(a)) {
1✔
783
                rest.add(a);
1✔
784
            }
785
        }
786
        return new Attributes(sLoc, nNsLoc, type, rest);
1✔
787
    }
788

789
    private static QName valueAsQName(Attr attribute) {
790
        if (attribute == null) {
1✔
791
            return null;
1✔
792
        }
793
        // split QName into prefix and local name
794
        String[] pieces = attribute.getValue().split(":");
1✔
795
        if (pieces.length < 2) {
1✔
796
            // unprefixed name
797
            pieces = new String[] { null, pieces[0] };
1✔
798
        } else if (pieces.length > 2) {
1!
799
            // actually, this is not a valid QName - be lenient
800
            pieces = new String[] {
×
801
                pieces[0],
802
                attribute.getValue().substring(pieces[0].length() + 1)
×
803
            };
804
        }
805
        if ("".equals(pieces[0])) {
1!
806
            pieces[0] = null;
×
807
        }
808
        return new QName(attribute.lookupNamespaceURI(pieces[0]), pieces[1]);
1✔
809
    }
810

811
    private static class Attributes {
812
        private final Attr schemaLocation;
813
        private final Attr noNamespaceSchemaLocation;
814
        private final Attr type;
815
        private final List<Attr> remainingAttributes;
816
        private Attributes(Attr schemaLocation, Attr noNamespaceSchemaLocation,
817
                           Attr type, List<Attr> remainingAttributes) {
1✔
818
            this.schemaLocation = schemaLocation;
1✔
819
            this.noNamespaceSchemaLocation = noNamespaceSchemaLocation;
1✔
820
            this.type = type;
1✔
821
            this.remainingAttributes = remainingAttributes;
1✔
822
        }
1✔
823
    }
824

825
    /**
826
     * Find the attribute with the same namespace and local name as a
827
     * given attribute in a list of attributes.
828
     */
829
    private static Attr findMatchingAttr(final List<Attr> attrs,
830
                                         final Attr attrToMatch) {
831
        final boolean hasNs = attrToMatch.getNamespaceURI() != null;
1✔
832
        final String nsToMatch = attrToMatch.getNamespaceURI();
1✔
833
        final String nameToMatch = hasNs ? attrToMatch.getLocalName()
1✔
834
            : attrToMatch.getName();
1✔
835
        for (Attr a : attrs) {
1✔
836
            if (((!hasNs && a.getNamespaceURI() == null)
1✔
837
                 ||
838
                 (hasNs && nsToMatch.equals(a.getNamespaceURI())))
1✔
839
                &&
840
                ((hasNs && nameToMatch.equals(a.getLocalName()))
1!
841
                 ||
842
                 (!hasNs && nameToMatch.equals(a.getName())))
1✔
843
                ) {
844
                return a;
1✔
845
            }
846
        }
1✔
847
        return null;
1✔
848
    }
849

850
    private static Map<Node, Integer> index(final Iterable<Node> nodes) {
851
        Map<Node, Integer> indices = new HashMap<Node, Integer>();
1✔
852
        int idx = 0;
1✔
853
        for (Node n : nodes) {
1✔
854
            indices.put(n, idx++);
1✔
855
        }
1✔
856
        return Collections.unmodifiableMap(indices);
1✔
857
    }
858
}
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