/* This file is licensed to You 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 org.xmlunit.builder; import org.w3c.dom.Attr; import org.w3c.dom.Node; import org.xmlunit.diff.Comparison; import org.xmlunit.diff.ComparisonController; import org.xmlunit.diff.ComparisonControllers; import org.xmlunit.diff.ComparisonFormatter; import org.xmlunit.diff.ComparisonListener; import org.xmlunit.diff.ComparisonResult; import org.xmlunit.diff.DOMDifferenceEngine; import org.xmlunit.diff.Diff; import org.xmlunit.diff.Difference; import org.xmlunit.diff.DifferenceEvaluator; import org.xmlunit.diff.DifferenceEvaluators; import org.xmlunit.diff.NodeMatcher; import org.xmlunit.input.CommentLessSource; import org.xmlunit.input.WhitespaceNormalizedSource; import org.xmlunit.input.WhitespaceStrippedSource; import org.xmlunit.util.Predicate; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Source; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; /** * DiffBuilder to create a {@link Diff} instance. * <p> * Valid inputs for control and test are all Objects supported by {@link Input#from(Object)}. * <p> * <b>Example Usage:</b> * * <pre> * String controlXml = "<a><b>Test Value</b></a>"; * String testXml = "<a>\n <b>\n Test Value\n </b>\n</a>"; * Diff myDiff = DiffBuilder.compare(Input.fromString(controlXml)).withTest(Input.fromString(testXml)) * .checkForSimilar() * .ignoreWhitespace() * .build(); * assertFalse("XML similar " + myDiff.toString(), myDiff.hasDifferences()); * </pre> */ public class DiffBuilder { private static final ComparisonResult[] CHECK_FOR_SIMILAR = new ComparisonResult[] { ComparisonResult.DIFFERENT}; private static final ComparisonResult[] CHECK_FOR_IDENTICAL = new ComparisonResult[] { ComparisonResult.SIMILAR, ComparisonResult.DIFFERENT}; private final Source controlSource; private Source testSource; private NodeMatcher nodeMatcher; private ComparisonController comparisonController = ComparisonControllers.Default; private DifferenceEvaluator differenceEvaluator = DifferenceEvaluators.Default; private List<ComparisonListener> comparisonListeners = new ArrayList<ComparisonListener>(); private List<ComparisonListener> differenceListeners = new ArrayList<ComparisonListener>(); private ComparisonResult[] comparisonResultsToCheck = CHECK_FOR_IDENTICAL; private Map<String, String> namespaceContext; private Predicate<Attr> attributeFilter; private Predicate<Node> nodeFilter; private ComparisonFormatter formatter; private boolean ignoreWhitespace; private boolean normalizeWhitespace; private boolean ignoreComments; private DocumentBuilderFactory documentBuilderFactory; /** * Create a DiffBuilder instance. * * @see DiffBuilder * @param controlSource the expected reference Result. */ private DiffBuilder(final Source controlSource) { this.controlSource = controlSource; } /** * Create a DiffBuilder from all kind of types supported by {@link Input#from(Object)}. * * @see DiffBuilder * @param control the expected reference document. */ public static DiffBuilder compare(final Object control) { final Source controlSource = getSource(control); return new DiffBuilder(controlSource); } /** * Set the Test-Source from all kind of types supported by {@link Input#from(Object)}. * * @param test the test document which must be compared with the control document. */ public DiffBuilder withTest(Object test) { testSource = getSource(test); return this; } private static Source getSource(Object object) { return Input.from(object).build(); } /** * Ignore whitespace by removing all empty text nodes and trimming the non-empty ones. */ public DiffBuilder ignoreWhitespace() { ignoreWhitespace = true; return this; } /** * Normalize Text-Elements by removing all empty text nodes and normalizing the non-empty ones. * <p> * "normalized" in this context means all whitespace characters are replaced by space characters and consecutive * whitespace characters are collapsed. * </p> */ public DiffBuilder normalizeWhitespace() { normalizeWhitespace = true; return this; } /** * Will remove all comment-Tags "<!-- Comment -->" from test- and control-XML before comparing. * * <p>Comments are ignored by applying an XSLT transformation on * the source which may reduce the effect of {@link * #withDocumentBuilderFactory}. If you need more control over the * transformation build the {@code Source} using a transformation * yourself, using {@link CommentLessSource#STYLE}.</p> */ public DiffBuilder ignoreComments() { ignoreComments = true; return this; } /** * Sets the strategy for selecting nodes to compare. * <p> * Example with {@link org.xmlunit.diff.DefaultNodeMatcher}: * <pre> * .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)) * </pre> * * @see org.xmlunit.diff.DifferenceEngine#setNodeMatcher(NodeMatcher) */ public DiffBuilder withNodeMatcher(final NodeMatcher nodeMatcher) { this.nodeMatcher = nodeMatcher; return this; } /** * Provide your own custom {@link DifferenceEvaluator} implementation. * This overwrites the Default DifferenceEvaluator. * If you want use your custom DifferenceEvaluator in combination with the default or another DifferenceEvaluator * you should use {@link DifferenceEvaluators#chain(DifferenceEvaluator...)} * or {@link DifferenceEvaluators#first(DifferenceEvaluator...)} to combine them: * <pre> * Diff myDiff = DiffBuilder.compare(control).withTest(test) * .withDifferenceEvaluator( * DifferenceEvaluators.chain( * DifferenceEvaluators.Default, * new MyCustomDifferenceEvaluator())) * .... * .build(); * </pre> */ public DiffBuilder withDifferenceEvaluator(final DifferenceEvaluator differenceEvaluator) { this.differenceEvaluator = differenceEvaluator; return this; } /** * Replace the {@link ComparisonControllers#Default} with your own ComparisonController. * <p> * Example use: * <pre> * Diff myDiff = DiffBuilder.compare(control).withTest(test) * .withComparisonController(ComparisonControllers.StopWhenDifferent) * .build(); * </pre> */ public DiffBuilder withComparisonController(final ComparisonController comparisonController) { this.comparisonController = comparisonController; return this; } /** * Registers a listener that is notified of each comparison. * * @see org.xmlunit.diff.DifferenceEngine#addComparisonListener(ComparisonListener) */ public DiffBuilder withComparisonListeners(final ComparisonListener... comparisonListeners) { this.comparisonListeners.addAll(Arrays.asList(comparisonListeners)); return this; } /** * Registers a listener that is notified of each comparison with * outcome other than {@link ComparisonResult#EQUAL}. * * @see org.xmlunit.diff.DifferenceEngine#addDifferenceListener(ComparisonListener) */ public DiffBuilder withDifferenceListeners(final ComparisonListener... comparisonListeners) { this.differenceListeners.addAll(Arrays.asList(comparisonListeners)); return this; } /** * check test source with the control source for similarity. * <p> * Example for Similar: The XML node "<a>Text</a>" and "<a><![CDATA[Text]]></a>" are * similar and the Test will not fail. * <p> * The rating, if a node is similar, will be done by the {@link DifferenceEvaluators#Default}. * See {@link #withDifferenceEvaluator(DifferenceEvaluator)} * <p> * Default is {@link #checkForIdentical()}. */ public DiffBuilder checkForSimilar() { comparisonResultsToCheck = CHECK_FOR_SIMILAR; return this; } /** * check test source with the control source for identically. * <p> * This is the Default. */ public DiffBuilder checkForIdentical() { comparisonResultsToCheck = CHECK_FOR_IDENTICAL; return this; } /** * Establish a namespace context that will be used in {@link * org.xmlunit.diff.Comparison.Detail#getXPath Comparison.Detail#getXPath}. * * <p>Without a namespace context (or with an empty context) the * XPath expressions will only use local names for elements and * attributes.</p> * * @param prefix2Uri mapping between prefix and namespace URI */ public DiffBuilder withNamespaceContext(Map<String, String> prefix2Uri) { namespaceContext = prefix2Uri; return this; } /** * Registers a filter for attributes. * * <p>Only attributes for which the predicate returns true are * part of the comparison. By default all attributes are * considered.</p> * * <p>The "special" namespace, namespace-location and * schema-instance-type attributes can not be ignored this way. * If you want to suppress comparison of them you'll need to * implement {@link DifferenceEvaluator}.</p> */ public DiffBuilder withAttributeFilter(Predicate<Attr> attributeFilter) { this.attributeFilter = attributeFilter; return this; } /** * Registers a filter for nodes. * * <p>Only nodes for which the predicate returns true are part of * the comparison. By default nodes that are not document types * are considered.</p> */ public DiffBuilder withNodeFilter(Predicate<Node> nodeFilter) { this.nodeFilter = nodeFilter; return this; } /** * Sets a non-default formatter for the differences found. */ public DiffBuilder withComparisonFormatter(ComparisonFormatter formatter) { this.formatter = formatter; return this; } /** * Sets the {@link DocumentBuilderFactory} to use when creating a * {@link Document} from the {@link Source}s to compare. * * <p>This is only used if the {@code Source}s used for control * and test not already are {@link * javax.xml.transform.dom.DOMSource}s.</p> * * @since XMLUnit 2.2.0 */ public DiffBuilder withDocumentBuilderFactory(DocumentBuilderFactory f) { documentBuilderFactory = f; return this; } /** * Compare the Test-XML {@link #withTest(Object)} with the Control-XML {@link #compare(Object)} and return the * collected differences in a {@link Diff} object. */ public Diff build() { final DOMDifferenceEngine d = new DOMDifferenceEngine(); final CollectResultsListener collectResultsListener = new CollectResultsListener(comparisonResultsToCheck); d.addDifferenceListener(collectResultsListener); if (nodeMatcher != null) { d.setNodeMatcher(nodeMatcher); } d.setDifferenceEvaluator(differenceEvaluator); d.setComparisonController(comparisonController); for (ComparisonListener comparisonListener : comparisonListeners) { d.addComparisonListener(comparisonListener); } for (ComparisonListener comparisonListener : differenceListeners) { d.addDifferenceListener(comparisonListener); } if (namespaceContext != null) { d.setNamespaceContext(namespaceContext); } if (attributeFilter != null) { d.setAttributeFilter(attributeFilter); } if (nodeFilter != null) { d.setNodeFilter(nodeFilter); } if (documentBuilderFactory != null) { d.setDocumentBuilderFactory(documentBuilderFactory); } d.compare(wrap(controlSource), wrap(testSource)); return formatter == null ? new Diff(controlSource, testSource, collectResultsListener.getDifferences()) : new Diff(controlSource, testSource, formatter, collectResultsListener.getDifferences()); } private Source wrap(final Source source) { Source newSource = source; if (ignoreWhitespace) { newSource = documentBuilderFactory != null ? new WhitespaceStrippedSource(newSource, documentBuilderFactory) : new WhitespaceStrippedSource(newSource); } if (normalizeWhitespace) { newSource = documentBuilderFactory != null ? new WhitespaceNormalizedSource(newSource, documentBuilderFactory) : new WhitespaceNormalizedSource(newSource); } if (ignoreComments) { newSource = new CommentLessSource(newSource); } return newSource; } private static final class CollectResultsListener implements ComparisonListener { private final List<Difference> results; private final EnumSet<ComparisonResult> comparisonResultsToCheck; public CollectResultsListener(final ComparisonResult... comparisonResultsToCheck) { results = new ArrayList<Difference>(); this.comparisonResultsToCheck = EnumSet.copyOf(Arrays.asList(comparisonResultsToCheck)); } @Override public void comparisonPerformed(final Comparison comparison, final ComparisonResult outcome) { if (comparisonResultsToCheck.contains(outcome)) { results.add(new Difference(comparison, outcome)); } } public List<Difference> getDifferences() { return Collections.unmodifiableList(results); } } }