/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.test.xml;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.jboss.xb.binding.Constants;
/**
* @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
* @version <tt>$Revision: 81036 $</tt>
*/
public class XmlDiff
{
public static final ErrorHandler ERROR_HANDLER = new DefErrorHandler();
public static final byte PRINT_ELEMENT = 0;
public static final byte PRINT_PARENT = 1;
public static final byte PRINT_ALL = 2;
private static final DocumentBuilderFactory FACTORY = DocumentBuilderFactory.newInstance();
static
{
FACTORY.setNamespaceAware(true);
FACTORY.setValidating(true);
}
private static final String INDENT = " ";
public static void main(String[] args) throws Exception
{
String xml1 =
"<ns1:e xmlns:ns1='http://ns' attr1='attr1_val' ns1:attr2='attr2_val'>\n" +
" <ns1:child1>\n" +
" <ns2:child2 xmlns:ns2='http://ns2' child2_attr='child2_attr_val'>child2_val</ns2:child2>\n" +
" </ns1:child1>\n" +
" text\n" +
"</ns1:e>";
String xml2 =
"<e xmlns='http://ns' attr1='attr1_val'" +
" xmlns:ns='http://ns' ns:attr2='attr2_val'>text" +
" <child1>" +
" <child2 xmlns='http://ns2' child2_attr='child2_attr_val'>child2_val</child2>" +
" </child1>" +
"</e>";
System.out.println(new XmlDiff().diff(xml1, xml2));
}
public XmlDiff()
{
this(PRINT_ALL, true);
}
public XmlDiff(byte print, boolean reformat)
{
this.print = print;
this.reformat = reformat;
}
private byte print = PRINT_ALL;
private boolean reformat = true;
public byte getPrint()
{
return print;
}
public void setPrint(byte print)
{
this.print = print;
}
public boolean isReformat()
{
return reformat;
}
public void setReformat(boolean reformat)
{
this.reformat = reformat;
}
/**
* Compares two XML contents and returns a diff if they are different or null if they are equal.
*
* @param expected expected XML content
* @param was actual XML content
* @return difference between XML contents or null if the contents are equal
*/
public String diff(String expected, String was)
{
return diff(expected, was, ERROR_HANDLER, null);
}
public String diff(String expected, String was, ErrorHandler eh)
{
return diff(expected, was, eh, null);
}
public String diff(String expected, String was, EntityResolver er)
{
return diff(expected, was, ERROR_HANDLER, er);
}
public String diff(String expected, String was, ErrorHandler eh, EntityResolver er)
{
DocumentBuilder documentBuilder = null;
try
{
documentBuilder = FACTORY.newDocumentBuilder();
}
catch(ParserConfigurationException e)
{
throw new IllegalStateException("Failed to create a document builder: " + e.getMessage());
}
if(eh != null)
{
documentBuilder.setErrorHandler(eh);
}
if(er != null)
{
documentBuilder.setEntityResolver(er);
}
Document expDoc = null;
try
{
expDoc = documentBuilder.parse(new InputSource(new StringReader(expected)));
}
catch(Exception e)
{
throw new IllegalStateException("Failed to parse expected XML\n" + expected + ": " + e.getMessage());
}
Document wasDoc = null;
try
{
wasDoc = documentBuilder.parse(new InputSource(new StringReader(was)));
}
catch(Exception e)
{
throw new IllegalStateException("Failed to parse XML\n" + was + ": " + e.getMessage());
}
Element expElement = expDoc.getDocumentElement();
Element wasElement = wasDoc.getDocumentElement();
return assertEquals(expElement, wasElement, expElement, wasElement);
}
private String assertEquals(Element exp, Element was, Element printAsExp, Element printAsWas)
{
QName expName = new QName(exp.getNamespaceURI(), exp.getLocalName());
QName wasName = new QName(was.getNamespaceURI(), was.getLocalName());
if(!expName.equals(wasName))
{
return fail("Expected name " + expName + " but was " + wasName, exp, was);
}
NamedNodeMap expAttrs = exp.getAttributes();
NamedNodeMap wasAttrs = was.getAttributes();
if(expAttrs == null && wasAttrs != null && hasNonIgnorableNs(wasAttrs))
{
return fail("Element " + expName + " doesn't have attributes", printAsExp, printAsWas);
}
else if(wasAttrs == null && expAttrs != null && hasNonIgnorableNs(expAttrs))
{
return fail("Element " + expName + " has attributes", printAsExp, printAsWas);
}
else if(expAttrs != null && wasAttrs != null)
{
String msg = assertAttrs(expAttrs, wasAttrs, printAsExp);
if(msg != null)
{
return fail(msg, printAsExp, printAsWas);
}
}
NodeList expChildren = exp.getChildNodes();
NodeList wasChildren = was.getChildNodes();
NodeList expTexts = getTextNodes(expChildren);
NodeList wasTexts = getTextNodes(wasChildren);
if(expTexts.getLength() > 0 && wasTexts.getLength() == 0)
{
return fail("Element " + expName + " has text content", printAsExp, printAsWas);
}
else if(expTexts.getLength() == 0 && wasTexts.getLength() > 0)
{
return fail("Element " + expName + " doesn't have text content", printAsExp, printAsWas);
}
// todo: should text content be concatenated before comparison?
else if(expTexts.getLength() != wasTexts.getLength())
{
return fail(
"Element " + expName + " has " + expTexts.getLength() + " text nodes (was " + wasTexts.getLength() + ")",
printAsExp,
printAsWas
);
}
else if(expTexts.getLength() > 0 && wasTexts.getLength() > 0)
{
for(int i = 0; i < expTexts.getLength(); ++i)
{
Text text = (Text)expTexts.item(i);
if(!containsText(text.getNodeValue(), wasTexts, i))
{
return fail("Element " + expName + " has text '" + text.getNodeValue() + "'", printAsExp, printAsWas);
}
}
}
NodeList expElems = sublist(expChildren, Node.ELEMENT_NODE);
NodeList wasElems = sublist(wasChildren, Node.ELEMENT_NODE);
if(expElems.getLength() > 0 && wasElems.getLength() == 0)
{
return fail("Element " + expName + " has child elements", printAsExp, printAsWas);
}
else if(expElems.getLength() == 0 && wasElems.getLength() > 0)
{
return fail("Element " + expName + " doesn't have child elements", printAsExp, printAsWas);
}
else if(expElems.getLength() != wasElems.getLength())
{
return fail("Element " +
expName +
" has " +
expElems.getLength() +
" child elements (was " +
wasElems.getLength() +
")",
printAsExp,
printAsWas
);
}
else if(expElems.getLength() > 0 && wasElems.getLength() > 0)
{
if(print == PRINT_PARENT)
{
printAsExp = exp;
printAsWas = was;
}
for(int i = 0; i < expElems.getLength(); ++i)
{
Element expChild = (Element)expElems.item(i);
Element wasChild = getElement(expChild.getNamespaceURI(), expChild.getLocalName(), wasElems, i);
if(wasChild == null)
{
return fail("Element " +
expName +
" has child element " +
new QName(expChild.getNamespaceURI(), expChild.getLocalName()),
printAsExp,
printAsWas
);
}
if(print == PRINT_ELEMENT)
{
printAsExp = expChild;
printAsWas = wasChild;
}
String diff = assertEquals(expChild, wasChild, printAsExp, printAsWas);
if(diff != null)
{
return diff;
}
}
}
return null;
}
private static Element getElement(String ns, String local, NodeList elements, int suggestedIndex)
{
if(suggestedIndex >= 0 && suggestedIndex < elements.getLength())
{
Element element = (Element)elements.item(suggestedIndex);
if((ns == null && element.getNamespaceURI() == null ||
ns != null && ns.equals(element.getNamespaceURI())
) &&
local.equals(element.getLocalName()))
{
return element;
}
}
for(int i = 0; i < elements.getLength(); ++i)
{
Element element = (Element)elements.item(i);
if((ns == null && element.getNamespaceURI() == null ||
ns != null && ns.equals(element.getNamespaceURI())
) &&
local.equals(element.getLocalName()))
{
return element;
}
}
return null;
}
private static boolean containsText(String text, NodeList textNodes, int suggestedIndex)
{
text = text.trim();
if(suggestedIndex >= 0)
{
Text textNode = (Text)textNodes.item(suggestedIndex);
String wasText = textNode.getNodeValue().trim();
if(text.equals(wasText))
{
return true;
}
}
for(int i = 0; i < textNodes.getLength(); ++i)
{
Text textNode = (Text)textNodes.item(i);
String wasText = textNode.getNodeValue().trim();
if(text.equals(wasText))
{
return true;
}
}
return false;
}
private static NodeList getTextNodes(NodeList list)
{
MutableNodeList result = new MutableNodeList();
for(int i = 0; i < list.getLength(); ++i)
{
Node node = list.item(i);
if(node.getNodeType() == Node.TEXT_NODE)
{
String text = node.getNodeValue();
if(text.trim().length() > 0)
{
result.add(node);
}
}
}
return result;
}
private static NodeList sublist(NodeList list, short nodeType)
{
MutableNodeList result = new MutableNodeList();
for(int i = 0; i < list.getLength(); ++i)
{
Node node = list.item(i);
if(node.getNodeType() == nodeType)
{
result.add(node);
}
}
return result;
}
private static String assertAttrs(NamedNodeMap attrsExp,
NamedNodeMap attrsWas,
Element printAsExp)
{
String result = assertSubset(attrsExp, attrsWas, printAsExp, true);
if(result == null)
{
result = assertSubset(attrsWas, attrsExp, printAsExp, false);
}
return result;
}
private static String assertSubset(NamedNodeMap attrsSubset,
NamedNodeMap attrsSet,
Element printAsExp,
boolean checkHave)
{
String msg = checkHave ? " has attribute " : " doesn't have attribute ";
QName expName = new QName(printAsExp.getNamespaceURI(), printAsExp.getLocalName());
for(int i = 0; i < attrsSubset.getLength(); ++i)
{
Attr attr = (Attr)attrsSubset.item(i);
String attrNs = attr.getNamespaceURI();
String localName = attr.getLocalName();
if(xsiNs(attrNs) && "type".equals(localName))
{
Attr wasAttr = (Attr)attrsSet.getNamedItemNS(attrNs, localName);
if(wasAttr == null)
{
return "Element " + expName + msg + new QName(attrNs, localName);
}
String typeName = attr.getValue();
int colon = typeName.indexOf(':');
if(colon != -1)
{
typeName = typeName.substring(colon);
}
if(!wasAttr.getValue().endsWith(typeName))
{
return "Element " + expName +
(checkHave ? " has xsi:type " : " doesn't have xsi:type ") +
attr.getValue();
}
//todo compare namespaces for xsi:types
}
else if(nonIgnorableNs(attrNs) || xsiNs(attrNs) && localName.equals("nil"))
{
Attr wasAttr = (Attr)attrsSet.getNamedItemNS(attrNs, localName);
if(wasAttr == null)
{
return "Element " + expName + msg + new QName(attrNs, localName);
}
if(!attr.getValue().equals(wasAttr.getValue()))
{
return "Attribute " +
new QName(attrNs, localName) +
" in element " +
expName +
" has value " + attr.getValue();
}
}
}
return null;
}
private static boolean hasNonIgnorableNs(NamedNodeMap nodeMap)
{
for(int i = 0; i < nodeMap.getLength(); ++i)
{
Node node = nodeMap.item(i);
if(nonIgnorableNs(node.getNamespaceURI()))
{
return true;
}
}
return false;
}
private static boolean nonIgnorableNs(String ns)
{
return ns == null ||
!ns.equals(Constants.NS_XML_SCHEMA)
&& !ns.equals(Constants.NS_XML_SCHEMA_INSTANCE)
&& !ns.equals(Constants.NS_XML_XMLNS);
}
private static boolean xsiNs(String ns)
{
return Constants.NS_XML_SCHEMA_INSTANCE.equals(ns);
}
private String fail(String msg, Element exp, Element was)
{
return msg + ". Expected\n" + toString(exp) + "\nbut was\n" + toString(was);
}
private String toString(Element e)
{
return append(e, new StringBuffer(), 0).toString();
}
private StringBuffer append(Element e, StringBuffer buf, int depth)
{
if(reformat && depth > 0)
{
buf.append('\n');
for(int i = 0; i < depth; ++i)
{
buf.append(INDENT);
}
}
buf.append('<');
if(e.getPrefix() != null && e.getPrefix().length() > 0)
{
buf.append(e.getPrefix()).append(':');
}
buf.append(e.getLocalName());
NamedNodeMap attrs = e.getAttributes();
if(attrs != null && attrs.getLength() > 0)
{
for(int i = 0; i < attrs.getLength(); ++i)
{
Attr attr = (Attr)attrs.item(i);
buf.append(' ')
.append(attr.getName())
.append('=')
.append('\'')
.append(attr.getValue())
.append('\'');
}
}
buf.append('>');
NodeList childNodes = e.getChildNodes();
boolean childElements = false;
for(int i = 0; i < childNodes.getLength(); ++i)
{
Node child = childNodes.item(i);
switch(child.getNodeType())
{
case Node.TEXT_NODE:
String chars = child.getNodeValue();
if(chars.trim().length() > 0)
{
buf.append(chars);
}
break;
case Node.ELEMENT_NODE:
append((Element)child, buf, depth + 1);
childElements = true;
break;
default:
throw new IllegalStateException("Node type is not supported: " + child.getNodeType());
}
}
if(reformat && childElements)
{
buf.append('\n');
for(int i = 0; i < depth; ++i)
{
buf.append(INDENT);
}
}
buf.append("</");
if(e.getPrefix() != null && e.getPrefix().length() > 0)
{
buf.append(e.getPrefix()).append(':');
}
buf.append(e.getLocalName())
.append('>');
return buf;
}
// Inner
private static final class MutableNodeList
implements NodeList
{
private List list = Collections.EMPTY_LIST;
public void add(Node node)
{
switch(list.size())
{
case 0:
list = Collections.singletonList(node);
break;
case 1:
list = new ArrayList(list);
default:
list.add(node);
}
}
public int getLength()
{
return list.size();
}
public Node item(int index)
{
return (Node)list.get(index);
}
}
public static final class DefErrorHandler
implements ErrorHandler
{
public static final byte IGNORE = 0;
public static final byte LOG = 1;
public static final byte FAIL = 3;
private byte warnEvent = IGNORE;
private byte errorEvent = IGNORE;
private byte fatalEvent = FAIL;
public void error(SAXParseException e) throws SAXException
{
handleEvent(warnEvent, e);
}
public void fatalError(SAXParseException e) throws SAXException
{
handleEvent(errorEvent, e);
}
public void warning(SAXParseException e) throws SAXException
{
handleEvent(fatalEvent, e);
}
private void handleEvent(byte event, SAXParseException e)
throws SAXException
{
switch(event)
{
case IGNORE:
break;
case LOG:
System.out.println(formatMessage(e));
break;
case FAIL:
String msg = formatMessage(e);
throw new SAXException(msg);
}
}
}
private static String formatMessage(SAXParseException exception)
{
StringBuffer buffer = new StringBuffer(50);
buffer.append(exception.getMessage()).append(" @ ");
String location = exception.getPublicId();
if(location != null)
{
buffer.append(location);
}
else
{
location = exception.getSystemId();
if(location != null)
{
buffer.append(location);
}
else
{
buffer.append("*unknown*");
}
}
buffer.append('[');
buffer.append(exception.getLineNumber()).append(',');
buffer.append(exception.getColumnNumber()).append(']');
return buffer.toString();
}
}