001: /*
002: ******************************************************************
003: Copyright (c) 2001-2007, Jeff Martin, Tim Bacon
004: All rights reserved.
005:
006: Redistribution and use in source and binary forms, with or without
007: modification, are permitted provided that the following conditions
008: are met:
009:
010: * Redistributions of source code must retain the above copyright
011: notice, this list of conditions and the following disclaimer.
012: * Redistributions in binary form must reproduce the above
013: copyright notice, this list of conditions and the following
014: disclaimer in the documentation and/or other materials provided
015: with the distribution.
016: * Neither the name of the xmlunit.sourceforge.net nor the names
017: of its contributors may be used to endorse or promote products
018: derived from this software without specific prior written
019: permission.
020:
021: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
022: "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
023: LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
024: FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
025: COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
026: INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
027: BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
028: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
029: CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
030: LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
031: ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
032: POSSIBILITY OF SUCH DAMAGE.
033:
034: ******************************************************************
035: */
036:
037: package org.custommonkey.xmlunit;
038:
039: import java.io.IOException;
040: import java.io.Reader;
041: import java.io.StringReader;
042: import javax.xml.transform.TransformerException;
043: import javax.xml.transform.dom.DOMSource;
044:
045: import org.custommonkey.xmlunit.exceptions.XMLUnitRuntimeException;
046:
047: import org.w3c.dom.Document;
048: import org.w3c.dom.Node;
049: import org.xml.sax.InputSource;
050: import org.xml.sax.SAXException;
051:
052: /**
053: * Compares and describes any difference between XML documents.
054: * Two documents are either:
055: * <br /><ul>
056: * <li><i>identical</i>: the content and sequence of the nodes in the documents
057: * are exactly the same</li>
058: * <li><i>similar</i>: the content of the nodes in the documents are the same,
059: * but minor differences exist e.g. sequencing of sibling elements, values of
060: * namespace prefixes, use of implied attribute values</li>
061: * <li><i>different</i>: the contents of the documents are fundamentally
062: * different</li>
063: * </ul>
064: * <br />
065: * The difference between compared documents is contained in a message buffer
066: * held in this class, accessible either through the <code>appendMessage</code>
067: * or <code>toString</code> methods. NB: When comparing documents, the
068: * comparison is halted as soon as the status (identical / similar / different)
069: * is known with certainty. For a list of all differences between the documents
070: * an instance of {@link DetailedDiff the DetailedDiff class} can be used
071: * instead.
072: * <br />Examples and more at <a href="http://xmlunit.sourceforge.net"/>xmlunit.sourceforge.net</a>
073: */
074: public class Diff implements DifferenceListener, ComparisonController {
075: private final Document controlDoc;
076: private final Document testDoc;
077: private boolean similar = true;
078: private boolean identical = true;
079: private boolean compared = false;
080: private boolean haltComparison = false;
081: private StringBuffer messages;
082: private DifferenceEngine differenceEngine;
083: private DifferenceListener differenceListenerDelegate;
084: private ElementQualifier elementQualifierDelegate;
085:
086: /**
087: * Construct a Diff that compares the XML in two Strings
088: */
089: public Diff(String control, String test) throws SAXException,
090: IOException {
091: this (new StringReader(control), new StringReader(test));
092: }
093:
094: /**
095: * Construct a Diff that compares the XML read from two Readers
096: */
097: public Diff(Reader control, Reader test) throws SAXException,
098: IOException {
099: this (
100: XMLUnit.buildDocument(XMLUnit.newControlParser(),
101: control), XMLUnit.buildDocument(XMLUnit
102: .newTestParser(), test));
103: }
104:
105: /**
106: * Construct a Diff that compares the XML in two Documents
107: */
108: public Diff(Document controlDoc, Document testDoc) {
109: this (controlDoc, testDoc, (DifferenceEngine) null);
110: }
111:
112: /**
113: * Construct a Diff that compares the XML in a control Document against the
114: * result of a transformation
115: */
116: public Diff(String control, Transform testTransform)
117: throws IOException, TransformerException, SAXException {
118: this (XMLUnit.buildControlDocument(control), testTransform
119: .getResultDocument());
120: }
121:
122: /**
123: * Construct a Diff that compares the XML read from two JAXP InputSources
124: */
125: public Diff(InputSource control, InputSource test)
126: throws SAXException, IOException {
127: this (
128: XMLUnit.buildDocument(XMLUnit.newControlParser(),
129: control), XMLUnit.buildDocument(XMLUnit
130: .newTestParser(), test));
131: }
132:
133: /**
134: * Construct a Diff that compares the XML in two JAXP DOMSources
135: */
136: public Diff(DOMSource control, DOMSource test) {
137: this (control.getNode().getOwnerDocument(), test.getNode()
138: .getOwnerDocument());
139: }
140:
141: /**
142: * Construct a Diff that compares the XML in two Documents using a specific
143: * DifferenceEngine
144: */
145: public Diff(Document controlDoc, Document testDoc,
146: DifferenceEngine comparator) {
147: this (controlDoc, testDoc, comparator,
148: new ElementNameQualifier());
149: }
150:
151: /**
152: * Construct a Diff that compares the XML in two Documents using a specific
153: * DifferenceEngine and ElementQualifier
154: */
155: public Diff(Document controlDoc, Document testDoc,
156: DifferenceEngine comparator,
157: ElementQualifier elementQualifier) {
158: this .controlDoc = getManipulatedDocument(controlDoc);
159: this .testDoc = getManipulatedDocument(testDoc);
160: this .elementQualifierDelegate = elementQualifier;
161: this .differenceEngine = comparator;
162: this .messages = new StringBuffer();
163: }
164:
165: /**
166: * Construct a Diff from a prototypical instance.
167: * Used by extension subclasses
168: * @param prototype a prototypical instance
169: */
170: protected Diff(Diff prototype) {
171: this (prototype.controlDoc, prototype.testDoc,
172: prototype.differenceEngine,
173: prototype.elementQualifierDelegate);
174: this .differenceListenerDelegate = prototype.differenceListenerDelegate;
175: }
176:
177: /**
178: * If {@link XMLUnit#getIgnoreWhitespace whitespace is ignored} in
179: * differences then manipulate the content to strip the redundant
180: * whitespace
181: * @param originalDoc a document making up one half of this difference
182: * @return the original document with redundant whitespace removed if
183: * differences ignore whitespace
184: */
185: private Document getWhitespaceManipulatedDocument(
186: Document originalDoc) {
187: if (!XMLUnit.getIgnoreWhitespace()) {
188: return originalDoc;
189: }
190: try {
191: Transform whitespaceStripper = XMLUnit
192: .getStripWhitespaceTransform(originalDoc);
193: return whitespaceStripper.getResultDocument();
194: } catch (TransformerException e) {
195: throw new XMLUnitRuntimeException(e.getMessage(), e
196: .getCause());
197: }
198: }
199:
200: /**
201: * Manipulates the given document according to the setting in the
202: * XMLUnit class.
203: *
204: * <p>This may involve:</p>
205: * <ul>
206: * <li>{@link XMLUnit.setIgnoreWhitespace stripping redundant
207: * whitespace}</li>
208: * <li>{@link XMLUnit.setIgnoreComments stripping comments}</li>
209: * <li>{@link XMLUnit.setNormalize normalizing Text nodes}</li>
210: * </ul>
211: *
212: * @param orig a document making up one half of this difference
213: * @return manipulated doc
214: */
215: private Document getManipulatedDocument(Document orig) {
216: return getNormalizedDocument(getCommentlessDocument(getWhitespaceManipulatedDocument(orig)));
217: }
218:
219: /**
220: * Removes all comment nodes if {@link XMLUnit.getIgnoreComments
221: * comments are ignored}.
222: *
223: * @param originalDoc a document making up one half of this difference
224: * @return manipulated doc
225: */
226: private Document getCommentlessDocument(Document orig) {
227: if (!XMLUnit.getIgnoreComments()) {
228: return orig;
229: }
230: try {
231: Transform commentStripper = XMLUnit
232: .getStripCommentsTransform(orig);
233: return commentStripper.getResultDocument();
234: } catch (TransformerException e) {
235: throw new XMLUnitRuntimeException(e.getMessage(), e
236: .getCause());
237: }
238: }
239:
240: private Document getNormalizedDocument(Document orig) {
241: if (!XMLUnit.getNormalize()) {
242: return orig;
243: }
244: Document d = (Document) orig.cloneNode(true);
245: d.normalize();
246: return d;
247: }
248:
249: /**
250: * Top of the recursive comparison execution tree
251: */
252: protected final void compare() {
253: if (compared) {
254: return;
255: }
256: getDifferenceEngine().compare(controlDoc, testDoc, this ,
257: elementQualifierDelegate);
258: compared = true;
259: }
260:
261: /**
262: * Return the result of a comparison. Two documents are considered
263: * to be "similar" if they contain the same elements and attributes
264: * regardless of order.
265: */
266: public boolean similar() {
267: compare();
268: return similar;
269: }
270:
271: /**
272: * Return the result of a comparison. Two documents are considered
273: * to be "identical" if they contain the same elements and attributes
274: * in the same order.
275: */
276: public boolean identical() {
277: compare();
278: return identical;
279: }
280:
281: /**
282: * Append a meaningful message to the buffer of messages
283: * @param appendTo the messages buffer
284: * @param expected
285: * @param actual
286: * @param control
287: * @param test
288: * @param difference
289: */
290: private void appendDifference(StringBuffer appendTo,
291: Difference difference) {
292: appendTo.append(' ').append(difference).append('\n');
293: }
294:
295: /**
296: * DifferenceListener implementation.
297: * If the {@link Diff#overrideDifferenceListener overrideDifferenceListener}
298: * method has been called then the interpretation of the difference
299: * will be delegated.
300: * @param difference
301: * @return a DifferenceListener.RETURN_... constant indicating how the
302: * difference was interpreted.
303: * Always RETURN_ACCEPT_DIFFERENCE if the call is not delegated.
304: */
305: public int differenceFound(Difference difference) {
306: int returnValue = RETURN_ACCEPT_DIFFERENCE;
307: if (differenceListenerDelegate != null) {
308: returnValue = differenceListenerDelegate
309: .differenceFound(difference);
310: }
311:
312: switch (returnValue) {
313: case RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL:
314: return returnValue;
315: case RETURN_IGNORE_DIFFERENCE_NODES_SIMILAR:
316: identical = false;
317: haltComparison = false;
318: break;
319: case RETURN_ACCEPT_DIFFERENCE:
320: identical = false;
321: if (difference.isRecoverable()) {
322: haltComparison = false;
323: } else {
324: similar = false;
325: haltComparison = true;
326: }
327: break;
328: default:
329: throw new IllegalArgumentException(
330: returnValue
331: + " is not a defined DifferenceListener.RETURN_... value");
332: }
333: if (haltComparison) {
334: messages.append("\n[different]");
335: } else {
336: messages.append("\n[not identical]");
337: }
338: appendDifference(messages, difference);
339: return returnValue;
340: }
341:
342: /**
343: * DifferenceListener implementation.
344: * If the {@link Diff#overrideDifferenceListener overrideDifferenceListener}
345: * method has been called then the call will be delegated
346: * otherwise a message is printed to <code>System.err</code>.
347: * @param control
348: * @param test
349: */
350: public void skippedComparison(Node control, Node test) {
351: if (differenceListenerDelegate != null) {
352: differenceListenerDelegate.skippedComparison(control, test);
353: } else {
354: System.err.println("DifferenceListener.skippedComparison: "
355: + "unhandled control node type=" + control
356: + ", unhandled test node type=" + test);
357: }
358: }
359:
360: /**
361: * ComparisonController implementation.
362: * @param afterDifference
363: * @return true if the difference is not recoverable and
364: * the comparison should be halted, or false if the difference
365: * is recoverable and the comparison can continue
366: */
367: public boolean haltComparison(Difference afterDifference) {
368: return haltComparison;
369: }
370:
371: /**
372: * Append the message from the result of this Diff instance to a specified
373: * StringBuffer
374: * @param toAppendTo
375: * @return specified StringBuffer with message appended
376: */
377: public StringBuffer appendMessage(StringBuffer toAppendTo) {
378: compare();
379: if (messages.length() == 0) {
380: messages.append("[identical]");
381: }
382: // fix for JDK1.4 backwards incompatibility
383: return toAppendTo.append(messages.toString());
384: }
385:
386: /**
387: * Get the result of this Diff instance as a String
388: * @return result of this Diff
389: */
390: public String toString() {
391: StringBuffer buf = new StringBuffer(getClass().getName());
392: appendMessage(buf);
393: return buf.toString();
394: }
395:
396: /**
397: * Override the <code>DifferenceListener</code> used to determine how
398: * to handle differences that are found.
399: * @param delegate the DifferenceListener instance to delegate handling to.
400: */
401: public void overrideDifferenceListener(DifferenceListener delegate) {
402: this .differenceListenerDelegate = delegate;
403: }
404:
405: /**
406: * Override the <code>ElementQualifier</code> used to determine which
407: * control and test nodes are comparable for this difference comparison.
408: * @param delegate the ElementQualifier instance to delegate to.
409: */
410: public void overrideElementQualifier(ElementQualifier delegate) {
411: this .elementQualifierDelegate = delegate;
412: }
413:
414: /**
415: * Lazily initializes the difference engine if it hasn't been set
416: * via a constructor.
417: */
418: private DifferenceEngine getDifferenceEngine() {
419: return differenceEngine == null ? new DifferenceEngine(this)
420: : differenceEngine;
421: }
422:
423: }
|