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

xmlunit / xmlunit / 667

pending completion
667

push

travis-ci-com

bodewig
add Cyclone DX SBOM generation to build

5824 of 6326 relevant lines covered (92.06%)

3.68 hits per line

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

98.31
/xmlunit-placeholders/src/main/java/org/xmlunit/placeholder/PlaceholderDifferenceEvaluator.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
package org.xmlunit.placeholder;
15

16
import java.util.Collections;
17
import java.util.HashMap;
18
import java.util.Map;
19
import java.util.ServiceLoader;
20
import java.util.regex.Matcher;
21
import java.util.regex.Pattern;
22
import javax.xml.namespace.QName;
23
import org.w3c.dom.Node;
24
import org.xmlunit.diff.Comparison;
25
import org.xmlunit.diff.ComparisonResult;
26
import org.xmlunit.diff.ComparisonType;
27
import org.xmlunit.diff.DifferenceEvaluator;
28
import org.xmlunit.util.Nodes;
29

30
/**
31
 * This class is used to add placeholder feature to XML comparison.
32
 *
33
 * <p><b>This class and the whole module are considered experimental
34
 * and any API may change between releases of XMLUnit.</b></p>
35
 *
36
 * <p>To use it, just add it with {@link
37
 * org.xmlunit.builder.DiffBuilder} like below</p>
38
 *
39
 * <pre>
40
 * Diff diff = DiffBuilder.compare(control).withTest(test).withDifferenceEvaluator(new PlaceholderDifferenceEvaluator()).build();
41
 * </pre>
42
 *
43
 * <p>Supported scenarios are demonstrated in the unit tests
44
 * (PlaceholderDifferenceEvaluatorTest).</p>
45
 *
46
 * <p>Default delimiters for placeholder are <code>${</code> and
47
 * <code>}</code>. Arguments to placeholders are by default enclosed
48
 * in {@code (} and {@code )} and separated by {@code ,} - whitespace
49
 * is significant, arguments are not quoted.</p>
50
 *
51
 * <p>To use custom delimiters (in regular expression), create
52
 * instance with the {@link #PlaceholderDifferenceEvaluator(String,
53
 * String)} or {@link #PlaceholderDifferenceEvaluator(String, String,
54
 * String, String, String)} constructors.</p>
55
 *
56
 * @since 2.6.0
57
 */
58
public class PlaceholderDifferenceEvaluator implements DifferenceEvaluator {
59
    public static final String PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX = Pattern.quote("${");
4✔
60
    public static final String PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX = Pattern.quote("}");
4✔
61
    /**
62
     * @since 2.7.0
63
     */
64
    public static final String PLACEHOLDER_DEFAULT_ARGS_OPENING_DELIMITER_REGEX = Pattern.quote("(");
4✔
65
    /**
66
     * @since 2.7.0
67
     */
68
    public static final String PLACEHOLDER_DEFAULT_ARGS_CLOSING_DELIMITER_REGEX = Pattern.quote(")");
4✔
69
    /**
70
     * @since 2.7.0
71
     */
72
    public static final String PLACEHOLDER_DEFAULT_ARGS_SEPARATOR_REGEX = Pattern.quote(",");
4✔
73

74
    private static final String PLACEHOLDER_PREFIX_REGEX = Pattern.quote("xmlunit.");
4✔
75
    private static final Map<String, PlaceholderHandler> KNOWN_HANDLERS;
76
    private static final String[] NO_ARGS = new String[0];
4✔
77

78
    static {
79
        Map<String, PlaceholderHandler> m = new HashMap<String, PlaceholderHandler>();
4✔
80
        for (PlaceholderHandler h : ServiceLoader.load(PlaceholderHandler.class)) {
4✔
81
            m.put(h.getKeyword(), h);
4✔
82
        }
4✔
83
        KNOWN_HANDLERS = Collections.unmodifiableMap(m);
4✔
84
    }
4✔
85

86
    private final Pattern placeholderRegex;
87
    private final Pattern argsRegex;
88
    private final String argsSplitter;
89

90
    /**
91
     * Creates a PlaceholderDifferenceEvaluator with default
92
     * delimiters {@link #PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX}
93
     * and {@link #PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX}.
94
     */
95
    public PlaceholderDifferenceEvaluator() {
96
        this(null, null);
4✔
97
    }
4✔
98

99
    /**
100
     * Creates a PlaceholderDifferenceEvaluator with custom delimiters.
101
     * @param placeholderOpeningDelimiterRegex regular expression for
102
     * the opening delimiter of placeholder, defaults to {@link
103
     * PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX}
104
     * if the parameter is null or blank
105
     * @param placeholderClosingDelimiterRegex regular expression for
106
     * the closing delimiter of placeholder, defaults to {@link
107
     * PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX}
108
     * if the parameter is null or blank
109
     */
110
    public PlaceholderDifferenceEvaluator(final String placeholderOpeningDelimiterRegex,
111
                                          final String placeholderClosingDelimiterRegex) {
112
        this(placeholderOpeningDelimiterRegex, placeholderClosingDelimiterRegex, null, null, null);
4✔
113
    }
4✔
114

115
    /**
116
     * Creates a PlaceholderDifferenceEvaluator with custom delimiters.
117
     * @param placeholderOpeningDelimiterRegex regular expression for
118
     * the opening delimiter of placeholder, defaults to {@link
119
     * PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX}
120
     * if the parameter is null or blank
121
     * @param placeholderClosingDelimiterRegex regular expression for
122
     * the closing delimiter of placeholder, defaults to {@link
123
     * PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX}
124
     * if the parameter is null or blank
125
     * @param placeholderArgsOpeningDelimiterRegex regular expression for
126
     * the opening delimiter of the placeholder's argument list, defaults to {@link
127
     * PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_OPENING_DELIMITER_REGEX}
128
     * if the parameter is null or blank
129
     * @param placeholderArgsClosingDelimiterRegex regular expression for
130
     * the closing delimiter of the placeholder's argument list, defaults to {@link
131
     * PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_CLOSING_DELIMITER_REGEX}
132
     * if the parameter is null or blank
133
     * @param placeholderArgsSeparatorRegex regular expression for the
134
     * delimiter between arguments inside of the placeholder's
135
     * argument list, defaults to {@link
136
     * PlaceholderDifferenceEvaluator#PLACEHOLDER_DEFAULT_ARGS_SEPARATOR_REGEX}
137
     * if the parameter is null or blank
138
     *
139
     * @since 2.7.0
140
     */
141
    public PlaceholderDifferenceEvaluator(String placeholderOpeningDelimiterRegex,
142
        String placeholderClosingDelimiterRegex,
143
        String placeholderArgsOpeningDelimiterRegex,
144
        String placeholderArgsClosingDelimiterRegex,
145
        String placeholderArgsSeparatorRegex) {
4✔
146
        if (placeholderOpeningDelimiterRegex == null
4✔
147
            || placeholderOpeningDelimiterRegex.trim().length() == 0) {
4✔
148
            placeholderOpeningDelimiterRegex = PLACEHOLDER_DEFAULT_OPENING_DELIMITER_REGEX;
4✔
149
        }
150
        if (placeholderClosingDelimiterRegex == null
4✔
151
            || placeholderClosingDelimiterRegex.trim().length() == 0) {
4✔
152
            placeholderClosingDelimiterRegex = PLACEHOLDER_DEFAULT_CLOSING_DELIMITER_REGEX;
4✔
153
        }
154
        if (placeholderArgsOpeningDelimiterRegex == null
4✔
155
            || placeholderArgsOpeningDelimiterRegex.trim().length() == 0) {
4✔
156
            placeholderArgsOpeningDelimiterRegex = PLACEHOLDER_DEFAULT_ARGS_OPENING_DELIMITER_REGEX;
4✔
157
        }
158
        if (placeholderArgsClosingDelimiterRegex == null
4✔
159
            || placeholderArgsClosingDelimiterRegex.trim().length() == 0) {
4✔
160
            placeholderArgsClosingDelimiterRegex = PLACEHOLDER_DEFAULT_ARGS_CLOSING_DELIMITER_REGEX;
4✔
161
        }
162
        if (placeholderArgsSeparatorRegex == null
4✔
163
            || placeholderArgsSeparatorRegex.trim().length() == 0) {
×
164
            placeholderArgsSeparatorRegex = PLACEHOLDER_DEFAULT_ARGS_SEPARATOR_REGEX;
4✔
165
        }
166

167
        placeholderRegex = Pattern.compile("(\\s*" + placeholderOpeningDelimiterRegex
4✔
168
            + "\\s*" + PLACEHOLDER_PREFIX_REGEX + "(.+)" + "\\s*"
169
            + placeholderClosingDelimiterRegex + "\\s*)");
170
        argsRegex = Pattern.compile("((.*)\\s*" + placeholderArgsOpeningDelimiterRegex
4✔
171
            + "(.+)"
172
            + "\\s*" + placeholderArgsClosingDelimiterRegex + "\\s*)");
173
        argsSplitter = placeholderArgsSeparatorRegex;
4✔
174
    }
4✔
175

176
    public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) {
177
        if (outcome == ComparisonResult.EQUAL) {
4✔
178
            return outcome;
4✔
179
        }
180

181
        Comparison.Detail controlDetails = comparison.getControlDetails();
4✔
182
        Node controlTarget = controlDetails.getTarget();
4✔
183
        Comparison.Detail testDetails = comparison.getTestDetails();
4✔
184
        Node testTarget = testDetails.getTarget();
4✔
185

186
        // comparing textual content of elements
187
        if (comparison.getType() == ComparisonType.TEXT_VALUE) {
4✔
188
            return evaluateConsideringPlaceholders((String) controlDetails.getValue(),
4✔
189
                (String) testDetails.getValue(), outcome);
4✔
190

191
        // "test document has no text-like child node but control document has"
192
        } else if (isMissingTextNodeDifference(comparison)) {
4✔
193
            return evaluateMissingTextNodeConsideringPlaceholders(comparison, outcome);
4✔
194

195
        // may be comparing TEXT to CDATA
196
        } else if (isTextCDATAMismatch(comparison)) {
4✔
197
            return evaluateConsideringPlaceholders(controlTarget.getNodeValue(), testTarget.getNodeValue(), outcome);
4✔
198

199
        // comparing textual content of attributes
200
        } else if (comparison.getType() == ComparisonType.ATTR_VALUE) {
4✔
201
            return evaluateConsideringPlaceholders((String) controlDetails.getValue(),
4✔
202
                (String) testDetails.getValue(), outcome);
4✔
203

204
        // "test document has no attribute but control document has"
205
        } else if (isMissingAttributeDifference(comparison)) {
4✔
206
            return evaluateMissingAttributeConsideringPlaceholders(comparison, outcome);
4✔
207

208
        // default, don't apply any placeholders at all
209
        } else {
210
            return outcome;
4✔
211
        }
212
    }
213

214
    private boolean isMissingTextNodeDifference(Comparison comparison) {
215
        return controlHasOneTextChildAndTestHasNone(comparison)
4✔
216
            || cantFindControlTextChildInTest(comparison);
4✔
217
    }
218

219
    private boolean controlHasOneTextChildAndTestHasNone(Comparison comparison) {
220
        Comparison.Detail controlDetails = comparison.getControlDetails();
4✔
221
        Node controlTarget = controlDetails.getTarget();
4✔
222
        Comparison.Detail testDetails = comparison.getTestDetails();
4✔
223
        return comparison.getType() == ComparisonType.CHILD_NODELIST_LENGTH &&
4✔
224
            Integer.valueOf(1).equals(controlDetails.getValue()) &&
4✔
225
            Integer.valueOf(0).equals(testDetails.getValue()) &&
4✔
226
            isTextLikeNode(controlTarget.getFirstChild());
4✔
227
    }
228

229
    private boolean cantFindControlTextChildInTest(Comparison comparison) {
230
        Node controlTarget = comparison.getControlDetails().getTarget();
4✔
231
        return comparison.getType() == ComparisonType.CHILD_LOOKUP
4✔
232
            && controlTarget != null && isTextLikeNode(controlTarget);
4✔
233
    }
234

235
    private ComparisonResult evaluateMissingTextNodeConsideringPlaceholders(Comparison comparison, ComparisonResult outcome) {
236
        Node controlTarget = comparison.getControlDetails().getTarget();
4✔
237
        String value;
238
        if (controlHasOneTextChildAndTestHasNone(comparison)) {
4✔
239
            value = controlTarget.getFirstChild().getNodeValue();
4✔
240
        } else {
241
            value = controlTarget.getNodeValue();
4✔
242
        }
243
        return evaluateConsideringPlaceholders(value, null, outcome);
4✔
244
    }
245

246
    private boolean isTextCDATAMismatch(Comparison comparison) {
247
        return comparison.getType() == ComparisonType.NODE_TYPE
4✔
248
            && isTextLikeNode(comparison.getControlDetails().getTarget())
4✔
249
            && isTextLikeNode(comparison.getTestDetails().getTarget());
4✔
250
    }
251

252
    private boolean isTextLikeNode(Node node) {
253
        short nodeType = node.getNodeType();
4✔
254
        return nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE;
4✔
255
    }
256

257
    private boolean isMissingAttributeDifference(Comparison comparison) {
258
        return comparison.getType() == ComparisonType.ELEMENT_NUM_ATTRIBUTES
4✔
259
            || (comparison.getType() == ComparisonType.ATTR_NAME_LOOKUP
4✔
260
                && comparison.getControlDetails().getTarget() != null
4✔
261
                && comparison.getControlDetails().getValue() != null);
4✔
262
    }
263

264
    private ComparisonResult evaluateMissingAttributeConsideringPlaceholders(Comparison comparison, ComparisonResult outcome) {
265
        if (comparison.getType() == ComparisonType.ELEMENT_NUM_ATTRIBUTES) {
4✔
266
            return evaluateAttributeListLengthConsideringPlaceholders(comparison, outcome);
4✔
267
        }
268
        String controlAttrValue = Nodes.getAttributes(comparison.getControlDetails().getTarget())
4✔
269
            .get((QName) comparison.getControlDetails().getValue());
4✔
270
        return evaluateConsideringPlaceholders(controlAttrValue, null, outcome);
4✔
271
    }
272

273
    private ComparisonResult evaluateAttributeListLengthConsideringPlaceholders(Comparison comparison,
274
        ComparisonResult outcome) {
275
        Map<QName, String> controlAttrs = Nodes.getAttributes(comparison.getControlDetails().getTarget());
4✔
276
        Map<QName, String> testAttrs = Nodes.getAttributes(comparison.getTestDetails().getTarget());
4✔
277

278
        int cAttrsMatched = 0;
4✔
279
        for (Map.Entry<QName, String> cAttr : controlAttrs.entrySet()) {
4✔
280
            String testValue = testAttrs.get(cAttr.getKey());
4✔
281
            if (testValue == null) {
4✔
282
                ComparisonResult o = evaluateConsideringPlaceholders(cAttr.getValue(), null, outcome);
4✔
283
                if (o != ComparisonResult.EQUAL) {
4✔
284
                    return outcome;
×
285
                }
286
            } else {
4✔
287
                cAttrsMatched++;
4✔
288
            }
289
        }
4✔
290
        if (cAttrsMatched != testAttrs.size()) {
4✔
291
            // there are unmatched test attributes
292
            return outcome;
4✔
293
        }
294
        return ComparisonResult.EQUAL;
4✔
295
    }
296

297
    private ComparisonResult evaluateConsideringPlaceholders(String controlText, String testText,
298
        ComparisonResult outcome) {
299
        final Matcher placeholderMatcher = placeholderRegex.matcher(controlText);
4✔
300
        if (placeholderMatcher.find()) {
4✔
301
            final String content = placeholderMatcher.group(2).trim();
4✔
302
            final Matcher argsMatcher = argsRegex.matcher(content);
4✔
303
            final String keyword;
304
            final String[] args;
305
            if (argsMatcher.find()) {
4✔
306
                keyword = argsMatcher.group(2).trim();
4✔
307
                args = argsMatcher.group(3).split(argsSplitter);
4✔
308
            } else {
309
                keyword = content;
4✔
310
                args = NO_ARGS;
4✔
311
            }
312
            if (isKnown(keyword)) {
4✔
313
                if (!placeholderMatcher.group(1).trim().equals(controlText.trim())) {
4✔
314
                    throw new RuntimeException("The placeholder must exclusively occupy the text node.");
4✔
315
                }
316
                return evaluate(keyword, testText, args);
4✔
317
            }
318
        }
319

320
        // no placeholder at all or unknown keyword
321
        return outcome;
4✔
322
    }
323

324
    private boolean isKnown(final String keyword) {
325
        return KNOWN_HANDLERS.containsKey(keyword);
4✔
326
    }
327

328
    private ComparisonResult evaluate(final String keyword, final String testText, final String[] args) {
329
        return KNOWN_HANDLERS.get(keyword).evaluate(testText, args);
4✔
330
    }
331
}
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