
com.github.tomakehurst.wiremock.matching.EqualToXmlPattern Maven / Gradle / Ivy
Show all versions of wiremock-standalone Show documentation
/*
* Copyright (C) 2016-2025 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.tomakehurst.wiremock.matching;
import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier;
import static com.github.tomakehurst.wiremock.common.Strings.isNullOrEmpty;
import static org.xmlunit.diff.ComparisonType.*;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.tomakehurst.wiremock.common.xml.Xml;
import com.github.tomakehurst.wiremock.stubbing.SubEvent;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.*;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xmlunit.XMLUnitException;
import org.xmlunit.builder.DiffBuilder;
import org.xmlunit.builder.Input;
import org.xmlunit.diff.*;
import org.xmlunit.placeholder.PlaceholderDifferenceEvaluator;
public class EqualToXmlPattern extends StringValuePattern {
private static final Set COUNTED_COMPARISONS =
Set.of(
ELEMENT_TAG_NAME,
SCHEMA_LOCATION,
NO_NAMESPACE_SCHEMA_LOCATION,
NODE_TYPE,
NAMESPACE_URI,
TEXT_VALUE,
PROCESSING_INSTRUCTION_TARGET,
PROCESSING_INSTRUCTION_DATA,
ELEMENT_NUM_ATTRIBUTES,
ATTR_VALUE,
CHILD_NODELIST_LENGTH,
CHILD_LOOKUP,
ATTR_NAME_LOOKUP);
private final DocumentBuilderFactory documentBuilderFactory;
private final Boolean enablePlaceholders;
private final String placeholderOpeningDelimiterRegex;
private final String placeholderClosingDelimiterRegex;
private final DifferenceEvaluator diffEvaluator;
private final Set exemptedComparisons;
private final Boolean ignoreOrderOfSameNode;
private final NamespaceAwareness namespaceAwareness;
private final Set countedComparisons;
private final Document expectedXmlDoc;
public EqualToXmlPattern(@JsonProperty("equalToXml") String expectedValue) {
this(expectedValue, null, null, null, null, null, null);
}
public EqualToXmlPattern(
@JsonProperty("equalToXml") String expectedValue,
@JsonProperty("enablePlaceholders") Boolean enablePlaceholders,
@JsonProperty("ignoreOrderOfSameNode") boolean ignoreOrderOfSameNode) {
this(expectedValue, enablePlaceholders, null, null, null, ignoreOrderOfSameNode, null);
}
@JsonCreator
public EqualToXmlPattern(
@JsonProperty("equalToXml") String expectedValue,
@JsonProperty("enablePlaceholders") Boolean enablePlaceholders,
@JsonProperty("placeholderOpeningDelimiterRegex") String placeholderOpeningDelimiterRegex,
@JsonProperty("placeholderClosingDelimiterRegex") String placeholderClosingDelimiterRegex,
@JsonProperty("exemptedComparisons") Set exemptedComparisons,
@JsonProperty("ignoreOrderOfSameNode") Boolean ignoreOrderOfSameNode,
@JsonProperty("namespaceAwareness") NamespaceAwareness namespaceAwareness) {
super(expectedValue);
documentBuilderFactory = newDocumentBuilderFactory(namespaceAwareness);
// Throw an exception if we can't parse the document
expectedXmlDoc = Xml.read(expectedValue, documentBuilderFactory);
this.enablePlaceholders = enablePlaceholders;
this.placeholderOpeningDelimiterRegex = placeholderOpeningDelimiterRegex;
this.placeholderClosingDelimiterRegex = placeholderClosingDelimiterRegex;
this.exemptedComparisons = exemptedComparisons;
this.ignoreOrderOfSameNode = ignoreOrderOfSameNode;
this.namespaceAwareness = namespaceAwareness;
Set comparisonsToExempt = new HashSet<>();
if (exemptedComparisons != null) {
comparisonsToExempt.addAll(exemptedComparisons);
}
this.countedComparisons =
COUNTED_COMPARISONS.stream()
.filter(e -> !comparisonsToExempt.contains(e))
.collect(Collectors.toSet());
IgnoreUncountedDifferenceEvaluator baseDifferenceEvaluator =
new IgnoreUncountedDifferenceEvaluator(comparisonsToExempt);
if (enablePlaceholders != null && enablePlaceholders) {
diffEvaluator =
DifferenceEvaluators.chain(
baseDifferenceEvaluator,
new PlaceholderDifferenceEvaluator(
placeholderOpeningDelimiterRegex, placeholderClosingDelimiterRegex));
} else {
diffEvaluator = baseDifferenceEvaluator;
}
}
public String getEqualToXml() {
return expectedValue;
}
@Override
public String getExpected() {
return Xml.prettyPrint(getValue());
}
public Boolean isEnablePlaceholders() {
return enablePlaceholders;
}
public Boolean isIgnoreOrderOfSameNode() {
return ignoreOrderOfSameNode;
}
public String getPlaceholderOpeningDelimiterRegex() {
return placeholderOpeningDelimiterRegex;
}
public String getPlaceholderClosingDelimiterRegex() {
return placeholderClosingDelimiterRegex;
}
public Set getExemptedComparisons() {
return exemptedComparisons;
}
public NamespaceAwareness getNamespaceAwareness() {
return namespaceAwareness;
}
@Override
public MatchResult match(final String value) {
return new MatchResult() {
@Override
public boolean isExactMatch() {
if (isNullOrEmpty(value)) {
return false;
}
try {
DiffBuilder diffBuilder =
DiffBuilder.compare(Input.from(expectedXmlDoc))
.withTest(value)
.withComparisonController(ComparisonControllers.StopWhenDifferent)
.ignoreWhitespace()
.withDifferenceEvaluator(diffEvaluator)
.withNodeMatcher(new OrderInvariantNodeMatcher(ignoreOrderOfSameNode))
.withDocumentBuilderFactory(documentBuilderFactory);
if (namespaceAwareness == NamespaceAwareness.LEGACY) {
// See NamespaceAwareness javadoc for details of why this is set here.
diffBuilder.ignoreComments();
}
Diff diff = diffBuilder.build();
return !diff.hasDifferences();
} catch (XMLUnitException e) {
appendSubEvent(SubEvent.warning(e.getMessage()));
notifier()
.info(
"Failed to process XML. "
+ e.getMessage()
+ "\nExpected:\n"
+ expectedValue
+ "\n\nActual:\n"
+ value);
return false;
}
}
@Override
public double getDistance() {
if (isNullOrEmpty(value)) {
return 1.0;
}
final AtomicInteger totalComparisons = new AtomicInteger(0);
final AtomicInteger differences = new AtomicInteger(0);
Diff diff;
try {
DiffBuilder diffBuilder =
DiffBuilder.compare(Input.from(expectedValue))
.withTest(value)
.ignoreWhitespace()
.withDifferenceEvaluator(diffEvaluator)
.withComparisonListeners(
(comparison, outcome) -> {
if (countedComparisons.contains(comparison.getType())
&& comparison.getControlDetails().getValue() != null) {
totalComparisons.incrementAndGet();
if (outcome == ComparisonResult.DIFFERENT) {
differences.incrementAndGet();
}
}
})
.withDocumentBuilderFactory(documentBuilderFactory);
if (namespaceAwareness == NamespaceAwareness.LEGACY) {
diffBuilder.ignoreComments();
}
diff = diffBuilder.build();
} catch (XMLUnitException e) {
notifier()
.info(
"Failed to process XML. "
+ e.getMessage()
+ "\nExpected:\n"
+ expectedValue
+ "\n\nActual:\n"
+ value);
return 1.0;
}
notifier()
.info(
StreamSupport.stream(diff.getDifferences().spliterator(), false)
.map(Object::toString)
.collect(Collectors.joining("\n")));
return differences.doubleValue() / totalComparisons.doubleValue();
}
};
}
private static DocumentBuilderFactory newDocumentBuilderFactory(
NamespaceAwareness namespaceAwareness) {
DocumentBuilderFactory factory = Xml.newDocumentBuilderFactory();
try {
factory.setFeature("http://apache.org/xml/features/include-comments", false);
factory.setFeature(
"http://xml.org/sax/features/namespaces",
namespaceAwareness == null || namespaceAwareness == NamespaceAwareness.STRICT);
} catch (ParserConfigurationException e) {
throwUnchecked(e);
}
return factory;
}
private static class IgnoreUncountedDifferenceEvaluator implements DifferenceEvaluator {
private final Set finalCountedComparisons;
public IgnoreUncountedDifferenceEvaluator(Set exemptedComparisons) {
finalCountedComparisons =
exemptedComparisons != null
? COUNTED_COMPARISONS.stream()
.filter(e -> !exemptedComparisons.contains(e))
.collect(Collectors.toSet())
: COUNTED_COMPARISONS;
}
@Override
public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) {
if (finalCountedComparisons.contains(comparison.getType())
&& comparison.getControlDetails().getValue() != null) {
return outcome;
}
return ComparisonResult.EQUAL;
}
}
public EqualToXmlPattern exemptingComparisons(ComparisonType... comparisons) {
return new EqualToXmlPattern(
expectedValue,
enablePlaceholders,
placeholderOpeningDelimiterRegex,
placeholderClosingDelimiterRegex,
new HashSet<>(Arrays.asList(comparisons)),
ignoreOrderOfSameNode,
namespaceAwareness);
}
public EqualToXmlPattern withNamespaceAwareness(NamespaceAwareness namespaceAwareness) {
return new EqualToXmlPattern(
expectedValue,
enablePlaceholders,
placeholderOpeningDelimiterRegex,
placeholderClosingDelimiterRegex,
exemptedComparisons,
ignoreOrderOfSameNode,
namespaceAwareness);
}
private static final class OrderInvariantNodeMatcher extends DefaultNodeMatcher {
private static Boolean secondaryOrderByTextContent;
public OrderInvariantNodeMatcher(Boolean secondaryOrderByTextContent) {
OrderInvariantNodeMatcher.secondaryOrderByTextContent = secondaryOrderByTextContent;
}
@Override
public Iterable> match(
Iterable controlNodes, Iterable testNodes) {
return super.match(sort(controlNodes), sort(testNodes));
}
private static Iterable sort(Iterable nodes) {
return StreamSupport.stream(nodes.spliterator(), false)
.sorted(getComparator())
.collect(Collectors.toList());
}
private static Comparator getComparator() {
if (Objects.nonNull(secondaryOrderByTextContent) && secondaryOrderByTextContent) {
return Comparator.comparing(Node::getLocalName).thenComparing(Node::getTextContent);
} else {
return Comparator.comparing(Node::getLocalName);
}
}
}
/**
* This enum represents how the pattern will treat XML namespaces when matching.
*
* {@link NamespaceAwareness#LEGACY} represents the old way that namespaces were treated. This
* had a lot of unpredictability and some behaviours were more of a side effect of other
* implementation details, rather than intentional. A key detail is that the original {@link
* DocumentBuilderFactory} was not namespace aware, but the XSLT transform performed by {@link
* DiffBuilder#ignoreComments()} seems to return a document that is semi-namespace aware, so some
* namespace aware functionality was available. Now {@link DiffBuilder#ignoreComments()} has been
* replaced by setting the {@link DocumentBuilderFactory} to ignore comment on read (much more
* performant and predictable), so is only used to produce the legacy namespace aware behaviour.
*
*
{@link NamespaceAwareness#STRICT} and {@link NamespaceAwareness#NONE} represent firmer, more
* intentional behaviour around how namespaces are handled. The details of how each option behaves
* are documented below:
*
*
{@link NamespaceAwareness#LEGACY} behaviour:
*
*
* - Namespace prefixes do not need to be bound to a namespace URI.
*
- Element namespace prefixes (and their corresponding namespace URIs) are ignored (e.g.
* `<th:thing>Match this</th:thing>` == `<st:thing>Match this</st:thing>`)
*
* - Element prefixes seem to effectively be totally removed from the document by the
* XSLT transform performed by {@link DiffBuilder#ignoreComments()} (and no namespace
* URI is assigned to the element).
*
* - Attributes are compared by their full name (i.e. namespace prefixes are NOT ignored)
* (e.g. `<thing th:attr="abc">Match this</thing>` != `<thing st:attr="abc">Match
* this</thing>`)
*
* - The XSLT transform performed by {@link DiffBuilder#ignoreComments()} does not
* assign a namespace URI to attributes, so XMLUnit uses the attribute's full name.
*
* - xmlns namespaced attributes are ignored (e.g. `<thing
* xmlns:th="https://thing.com">Match this</thing>` == `<thing
* xmlns:st="https://stuff.com">Match this</thing>`)
*
* - XMLUnit ignores all attributes namespaced to http://www.w3.org/2000/xmlns/, which
* all xmlns prefixed attributes are assigned to by the XSLT transform performed by
* {@link DiffBuilder#ignoreComments()}.
*
* - Element default namespace attributes (i.e. `xmlns` attributes) are NOT ignored unless
* NAMESPACE_URI comparison type is explicitly excluded (e.g. `<thing
* xmlns="https://thing.com">Match this</thing>` != `<thing
* xmlns="https://stuff.com">Match this</thing>`)
*
* - Like xmlns namespaced attributes, XMLUnit ignores all attributes namespaced to
* http://www.w3.org/2000/xmlns/, which all xmlns attributes are assigned to by the
* XSLT transform performed by {@link DiffBuilder#ignoreComments()}.
*
- The difference between default xmlns attributes and xmlns prefixed
* attributes is that the XSLT transform performed by {@link
* DiffBuilder#ignoreComments()} assigns the namespace URI of default xmlns attributes
* to the attributed element, which is why matching will fail (unless NAMESPACE_URI
* comparison type is explicitly excluded).
*
*
*
* {@link NamespaceAwareness#STRICT} behaviour:
*
*
* - Namespace prefixes need to be bound to a namespace URI.
*
- Element and attribute namespace URIs are compared, but their prefixes are ignored.
*
* - Namespace URIs can be explicitly excluded. Although, due to how XMLUnit's engine is
* implemented, excluding NAMESPACE_URI does not work with attributes (see XMLUnit issue).
*
* - The namespaces defined by xmlns namespaced attributes are compared, but the attributes
* themselves are ignored (e.g. `<thing xmlns:th="https://thing.com">Match
* this</thing>` == `<thing xmlns:st="https://stuff.com">Match this</thing>`)
*
* - XMLUnit ignores all attributes namespaced to http://www.w3.org/2000/xmlns/, which
* all default and prefixed xmlns attributes are assigned to by when the document
* builder factory is namespace aware.
*
*
*
* {@link NamespaceAwareness#NONE} behaviour:
*
*
* - Namespace prefixes do not need to be bound to a namespace URI.
*
- Element and attribute are compared by their full name and all namespace URIs are ignored.
*
- xmlns attributes are not ignored and are treated like any other attribute.
*
*/
public enum NamespaceAwareness {
STRICT,
LEGACY,
NONE,
}
}