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

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

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

88.75
/extensions/modules/xmldiff/src/main/java/org/exist/xquery/modules/xmldiff/Compare.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 */
24
package org.exist.xquery.modules.xmldiff;
25

26
import io.lacuna.bifurcan.IMap;
27
import org.exist.dom.persistent.NodeProxy;
28
import org.exist.xquery.functions.map.MapType;
29
import org.w3c.dom.Node;
30
import org.xmlunit.builder.DiffBuilder;
31
import org.xmlunit.builder.Input;
32
import org.xmlunit.diff.Diff;
33

34
import org.exist.xquery.*;
35
import org.exist.xquery.value.*;
36

37
import javax.annotation.Nullable;
38
import javax.xml.transform.Source;
39

40
import static org.exist.xquery.FunctionDSL.*;
41
import static org.exist.xquery.modules.xmldiff.XmlDiffModule.functionSignature;
42

43
/**
44
 * Module for comparing XML documents and nodes.
45
 *
46
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
47
 */
48
public class Compare extends BasicFunction {
49

50
    private static final StringValue EQUIVALENT_MAP_KEY = new StringValue("equivalent");
1✔
51
    private static final StringValue POSITION_MAP_KEY = new StringValue("position");
1✔
52
    private static final StringValue MESSAGE_MAP_KEY = new StringValue("message");
1✔
53

54
    private static final FunctionParameterSequenceType FS_PARAM_NODE_SET_1 = optManyParam("node-set-1", Type.NODE, "The first node set.");
1✔
55
    private static final FunctionParameterSequenceType FS_PARAM_NODE_SET_2 = optManyParam("node-set-2", Type.NODE, "The second node set.");
1✔
56

57
    private static final String FNS_COMPARE = "compare";
58
    private static final String FNS_DIFF = "diff";
59

60
    public static final FunctionSignature FS_COMPARE = functionSignature(
1✔
61
            FNS_COMPARE,
62
            "Compares two nodes sets to determine their equivalence." +
63
                    "Equivalence is determined in 3 stages, first by sequence length, then equivalent Node types, and finally by XMLUnit Diff.",
64
            returns(Type.BOOLEAN, "Returns true if the node sets $node-set-1 and $node-set-2 are equal, false otherwise. " +
1✔
65
                    "This function is a simplified version of: " + XmlDiffModule.PREFIX + ":" + FNS_DIFF + "#2 that only returns true or false."),
66
            FS_PARAM_NODE_SET_1,
67
            FS_PARAM_NODE_SET_2
68
    );
69

70
    public static final FunctionSignature FS_DIFF = functionSignature(
1✔
71
            FNS_DIFF,
72
            "Reports on the differences between two nodes sets to determine their equality." +
73
                    "Equality is determined in 3 stages, first by sequence length, then equivalent Node types, and finally by XMLUnit Diff for Document and Element nodes, or fn:deep-equals for all other node types.",
74
            returns(Type.MAP_ITEM, "Returns a map(xs:string, xs:anyAtomicType). When the node sets are equivalent the map is: map {'equivalent': fn:true() }. When the nodesets are not equivalent, the map is structured like: map {'equivalent': fn:false(), 'position': xs:integer, 'message': xs:string}."),
1✔
75
            FS_PARAM_NODE_SET_1,
76
            FS_PARAM_NODE_SET_2
77
    );
78

79
    public Compare(final XQueryContext context, final FunctionSignature signature) {
80
        super(context, signature);
1✔
81
    }
1✔
82

83
    @Override
84
    public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
85
        final Sequence nodeSet1 = args[0];
1✔
86
        final Sequence nodeSet2 = args[1];
1✔
87

88
        final int itemCount1 = nodeSet1.getItemCount();
1✔
89
        final int itemCount2 = nodeSet2.getItemCount();
1✔
90

91
        // first determination - are the sequences of the same length?
92
        if (itemCount1 != itemCount2) {
1✔
93
            if (isCalledAs(FNS_COMPARE)) {
1!
94
                return BooleanValue.FALSE;
1✔
95
            } else {
96
                return falseMapResult(Math.min(itemCount1, itemCount2), "Sequences are of different lengths: fn:length($node-set-1) eq " + itemCount1 + ", fn:length($node-set-2) eq " + itemCount2 + ".");
×
97
            }
98
        }
99

100
        // second determination - do the sequences contain the same types?
101
        for (int i = 0; i < itemCount1; i++) {
1✔
102
            final Item item1 = nodeSet1.itemAt(i);
1✔
103
            final Item item2 = nodeSet2.itemAt(i);
1✔
104

105
            if (item1.getType() != item2.getType()) {
1✔
106
                if (isCalledAs(FNS_COMPARE)) {
1!
107
                    return BooleanValue.FALSE;
1✔
108
                } else {
109
                    return falseMapResult(i + 1, "Items are of different types: $node-set-1[" + i + "] as " + Type.getTypeName(item1.getType()) + ", $node-set-2[" + i + "] as " + Type.getTypeName(item2.getType()) + ".");
×
110
                }
111
            }
112
        }
113

114
        // third determination - does XMLUnit consider each node in the sequences to be equal
115
        for (int i = 0; i < itemCount1; i++) {
1✔
116
            final Node node1 = toNode(nodeSet1.itemAt(i));
1✔
117
            final Node node2 = toNode(nodeSet2.itemAt(i));
1✔
118

119
            if (node1 == null || node2 == null) {
1!
120
                throw new XPathException(this, XmlDiffModule.UNSUPPORTED_DOM_IMPLEMENTATION, "Unable to determine DOM implementation of node set item");
×
121
            }
122

123
            final Source expected = Input.fromNode(node1).build();
1✔
124
            final Source actual = Input.fromNode(node2).build();
1✔
125

126
            final Diff diff = DiffBuilder.compare(expected).withTest(actual)
1✔
127
                    .checkForIdentical()
1✔
128
                    .build();
1✔
129

130
            if (diff.hasDifferences()) {
1✔
131
                if (isCalledAs(FNS_COMPARE)) {
1✔
132
                    return BooleanValue.FALSE;
1✔
133
                } else {
134
                    return falseMapResult(i + 1, diff.toString());
1✔
135
                }
136
            }
137
        }
138

139
        if (isCalledAs(FNS_COMPARE)) {
1✔
140
            return BooleanValue.TRUE;
1✔
141
        } else {
142
            return trueMapResult();
1✔
143
        }
144
    }
145

146
    private MapType trueMapResult() {
147
        return new MapType(getContext(), getContext().getDefaultCollator(), EQUIVALENT_MAP_KEY, BooleanValue.TRUE);
1✔
148
    }
149

150
    private MapType falseMapResult(final int sequencePosition, final String message) {
151
        final IMap<AtomicValue, Sequence> linearMap = MapType.newLinearMap(getContext().getDefaultCollator());
1✔
152
        linearMap.put(EQUIVALENT_MAP_KEY, BooleanValue.FALSE);
1✔
153
        linearMap.put(POSITION_MAP_KEY, new IntegerValue(sequencePosition));
1✔
154
        linearMap.put(MESSAGE_MAP_KEY, new StringValue(message.trim()));
1✔
155
        return new MapType(getContext(), linearMap.forked(), Type.STRING);
1✔
156
    }
157

158
    private static @Nullable Node toNode(final Item item) {
159
        if (item instanceof Node) {
1✔
160
            return (Node) item;
1✔
161
        }
162

163
        if (item instanceof NodeProxy) {
1!
164
            return ((NodeProxy) item).getNode();
1✔
165
        }
166

167
        return null;
×
168
    }
169
}
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

© 2026 Coveralls, Inc