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...

No comments: