Sunday, September 12, 2010

XML comparison

Today I needed an important feature that for various reasons can’t be fully implemented, the comparison of 2 XMLs which vary slightly. I did similar things before, but every time there's a new twist...
Let me begin by defining the “vary slightly”. The XMLs are something like
Original:

<XML>
<T></T>
<T ID=”1”>
<props>
<prop key=”A1” value=”X”></prop>
<prop key=”B1” value=”Y”></prop>
</props>
</T>
<T></T>
<S ID=”A”></S>
</XML>

And new:

<XML>
<T ID=”1”>
<props>
<prop key=”A1” value=”X”></prop>
<prop key=”B1” value=”Y”></prop>
</T>
<S ID=”A”></S>
</XML>

Any “normal” comparison would jump claiming the number of nodes doesn’t match. While being 100% correct it’s utterly useless for me because those are empty nodes which the XSL parser removes on the next phase. So why not use the XSL as a validator? Well we could, but, we want to check the XSL too…
A quick googling showed me that there’s no ready from the box solution, though apparently many need it.
Here’s my small contribution to whomever may need it. It’s not perfect. I took the liberty to tweak it a bit to my needs (only the first attribute is relevant for the xpath.) On the plus sign, it’s thread-safe, as usual I need my things to work in multi-threaded environments…

public class XML_Comparer
{
private static object lock_obj = new object();
private static int CompareError = 0;

private System.Collections.ArrayList l_IgnoredAttribs = new System.Collections.ArrayList();
private string l_status = "";
private string status_log = "";

public System.Xml.XmlDocument OriginalDocument { get; set; }
public System.Xml.XmlDocument NewDocument { get; set; }
public System.Collections.ArrayList IgnoredAttribs
{
get
{
return l_IgnoredAttribs;
}
}
public string BasePath { get; set; }
public string Results { get; private set; }
public string Status
{
get
{
lock (this)
{
return l_status;
}
}
private set
{
lock (this)
{
l_status = value;
status_log += value + "\r\n";
}
}
}

private void CompareXMLs()
{
Status = "Comparisson started";
string res = CompareXMLNodes(NewDocument, OriginalDocument);
if (res != "")
{
lock (lock_obj)
{
CompareError++;
NewDocument.Save(BasePath + CompareError.ToString() + "_new.xml");
OriginalDocument.Save(BasePath + CompareError.ToString() + "_old.xml");
Results = BasePath + CompareError.ToString() + "\r\n" + res + "\r\n" + Results;
}
}
Status = "Comparisson complete";
}
private string CompareXMLNodes(System.Xml.XmlNode new_nd, System.Xml.XmlNode orig_nd)
{
if ((new_nd == null) || (orig_nd == null))
{
Status = "Incorrect comparison!";
throw new Exception("Incorrect comparison!");
}
Status = "Comparing " + orig_nd.Name;
StringBuilder res = new StringBuilder();
if (orig_nd.Attributes != null)
{
System.Xml.XmlAttribute atr = null;
for (int i = 0; i < orig_nd.Attributes.Count; i++)
{
if (!l_IgnoredAttribs.Contains(orig_nd.Attributes[i].Name))
{
if (new_nd.Attributes != null)
atr = new_nd.Attributes[orig_nd.Attributes[i].Name];
if (atr == null)
{
res.AppendLine("Missing attribute: " + orig_nd.Attributes[i].Name);
}
else if (orig_nd.Attributes[i].InnerText != atr.InnerText)
{
res.AppendLine("Attribute " + orig_nd.Attributes[i].Name + " - values don't match (" + orig_nd.Attributes[i].InnerText + "!=" + atr.InnerText + ")");
}
}
}
}
if (orig_nd.ChildNodes.Count == 0)
{
if (orig_nd.InnerText != new_nd.InnerText)
{
res.Append(orig_nd.InnerText + "!=" + new_nd.InnerText);
}
}
else
{
foreach (System.Xml.XmlNode nd in orig_nd.ChildNodes)
{
string path = nd.Name;
if (nd.Attributes.Count > 0)
{
path += "[";
{
path += "@" + nd.Attributes[0].Name + "='" + nd.Attributes[0].InnerText + "'";
}
path += "]";
}
System.Xml.XmlNode new_child_nd = new_nd.SelectSingleNode(path);
if (new_child_nd != null)
{
string nd_res = CompareXMLNodes(new_child_nd, nd);
if (nd_res != "")
res.AppendLine(nd_res);
}
}
}
if (res.Length > 0)
{
return "Node " + orig_nd.Name + "\r\n" + res.ToString();
}
else
{
return "";
}
}

public XML_Comparer()
{
//
}
public XML_Comparer(string orig_xml, string new_xml)
{
OriginalDocument = new System.Xml.XmlDocument();
OriginalDocument.LoadXml(orig_xml);
NewDocument = new System.Xml.XmlDocument();
NewDocument.LoadXml(orig_xml);
}

public void Init()
{
lock (this)
{
status_log = "";
}
l_IgnoredAttribs = new System.Collections.ArrayList();
Status = "Initialized";
}
public void Init(string IgnoredAttribs_filename)
{
Init();
System.IO.StreamReader sr = new System.IO.StreamReader(IgnoredAttribs_filename);
while (!sr.EndOfStream)
{
string line = sr.ReadLine();
IgnoredAttribs.Add(line);
}
sr.Close();
}
public void Init(System.Collections.ArrayList IgnoredAttribs_List)
{
Init();
l_IgnoredAttribs = IgnoredAttribs_List;
}
public string Run()
{
CompareXMLs();
if (Status == "Comparisson complete")
{
return Results;
}
else
{
return "ERROR";
}
}
}

This time I just copy-pasted the full class without any further explanation, hopefully it'd help.

P.S.
There might be some error in the cases due to Word's idea of doing things without being asked...