/*
* Copyright IBM Corp. 2011
*
* Licensed 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 xsp.extlib.designer.junit.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
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 com.ibm.commons.util.StringUtil;
public class XMLCompareUtils {
/**
* This method will compare the attributes of a given src element with the attributes of a given dest element.
* Both must contain the same attributes, but you can specify a String array of attribute names whose values
* are ignored. Both elements must have the ignore attributes, their contents can be different and they will
* still be considered equal.
* @param src - the src element whose attributes are to be compared
* @param dest - the dest element whose attributes are to be compared
* @param ignore - the string array of attributes whose values are to be ignored during the compare.
* @return true if the attributes of both nodes meet the criteria of being equal, false otherwise.
*/
private static boolean compareAttributes (Element src, Element dest, String[] ignore) {
NamedNodeMap srcAttrs = src.getAttributes();
if (srcAttrs.getLength() != dest.getAttributes().getLength())
return false;
for (int ctr=0; ctr<srcAttrs.getLength(); ctr++) {
Node srcAttr = srcAttrs.item(ctr);
String name = srcAttr.getNodeName();
if (Arrays.binarySearch(ignore, name) < 0) {
Node destAttr = dest.getAttributeNode(name);
if (destAttr == null || !srcAttr.isEqualNode(destAttr)) {
return false;
}
}
}
return true;
}
/**
* More exhaustive comparison of two elements. Does not assume child node order is the same in src and dest.
* @param src
* @param dest
* @param ignoreContentsOfNodes - an array of nodes that only need to be present. Their children do not take part in the comparison.
* @param ignoreExistenceOfNodes - an array of nodes that will be ignored. If they are present in one element and not in the other, the two elements are still considered equal
* @param ignoreAttrs - an array of attributes to ignore. The "value" of these attributes will be ignored when comparing any node or child node.
* @return
*/
private static boolean deepCompareElements (Node src, Node dest, String[] ignoreContentsOfNodes, String[] ignoreExistenceOfNodes, String[] ignoreAttrs) {
//first check to see if the basics of the two nodes are equal
if(areIndividualNodeEqual(src, dest, ignoreContentsOfNodes, ignoreAttrs)){
if (Arrays.binarySearch(ignoreContentsOfNodes, src.getNodeName()) >=0) {
//if this is a node we are ignoring, then no need to check its children
return true;
}
//we need the security of using an iterator when removing child nodes, so instead of using
//getChildNodes and dealing with a NodeList which does not implement Iterator, we use a helper
//method to get the children in an ArrayList<Node>.
ArrayList<Node> srcNodes = getChildNodes(src);
ArrayList<Node> destNodes = getChildNodes(dest);
if(srcNodes.isEmpty() && destNodes.isEmpty()){
//no children and all the other node info matches, so these are equal
return true;
}
if(!srcNodes.isEmpty() && !destNodes.isEmpty()){
Iterator<Node> srcNodeIterator = srcNodes.iterator();
while(srcNodeIterator.hasNext()){
Node srcNode = srcNodeIterator.next();
boolean found = findAndRemoveMatchingNode(srcNode, destNodes, ignoreContentsOfNodes, ignoreExistenceOfNodes, ignoreAttrs);
if(found){
//we found a matching node in the destNodes array. It will have been removed from destNodes
//so remove from srcNodes as well now
srcNodeIterator.remove();
}
else{
//we failed to find a match so either they not the same, or this is an ignoreExistenceOfNode
//if it is an ignoreExistneceOfNode then we can just remove it and consider it found.
if(Arrays.binarySearch(ignoreExistenceOfNodes, srcNode.getNodeName()) >= 0){
srcNodeIterator.remove();
}
else{
//failed to find a match and not an ignoreExistencOf node, so return false.
return false;
}
}
}
//at this point we should have gone through all the nodes in both maps.
//If both maps are empty, then we have found all nodes in both and they are equal.
if(srcNodes.isEmpty() && destNodes.isEmpty()){
return true;
}
else{
return false;
}
}
}
//basic node properties are not equal, so not the same.
return false;
}
/**
* This method will get the child nodes of a given node and return them in an ArrayList
* instead of a NodeList. NodeList does not implement Iterator, which is needed to make
* the compare code safer.
* @param parentNode
* @return an ArrayList of childNodes if the parentNode has childNodes. If there are no children an empty ArrayList is returned.
*/
private static ArrayList<Node> getChildNodes(Node parentNode){
ArrayList<Node> childNodesArray = new ArrayList<Node>();
if(null != parentNode){
NodeList childNodes = parentNode.getChildNodes();
if(null != childNodes && childNodes.getLength()>0){
for(int i=0; i<childNodes.getLength(); i++){
Node child = childNodes.item(i);
if(null != child){
childNodesArray.add(child);
}
}
}
}
return childNodesArray;
}
/**
* This method will search through the destNodes to try find a matching node to srcNode.
* The matched node will have to have identical children to N levels to be considered a match.
* Once a match is found, the node is removed from destNodes.
* @param srcNode
* @param destNodes
* @return true if match was found and removed, false otherwise.
*/
private static boolean findAndRemoveMatchingNode(Node srcNode, ArrayList<Node> destNodes, String[] ignoreContentsOfNodes,String[] ignoreExistenceOfNodes, String[] ignoreAttrs){
Iterator<Node> destNodesIterator = destNodes.iterator();
while(destNodesIterator.hasNext()){
Node destNode = destNodesIterator.next();
if(deepCompareElements(srcNode, destNode, ignoreContentsOfNodes, ignoreExistenceOfNodes, ignoreAttrs)){
//we have found a matching node, remove it from destNodes
destNodesIterator.remove();
return true;
}
else{
//we failed to find a match. Check if this is an ignoreExistenceOfNode, in which case we can just ignore it not having a match.
if(Arrays.binarySearch(ignoreExistenceOfNodes, destNode.getNodeName()) >= 0){
destNodesIterator.remove();
}
}
}
return false;
}
/**
* This method will check that two nodes are equal.
* For nodes to be considered equal.
* The nodes must be of the same type
* The following node properties must be equal:
* nodeName,
* localName,
* namespaceURI,
* prefix,
* nodeValue
* The attributes NamedNodeMaps must be equal (attributes in ignoreAttrs are ignored)
* The childNodes NodeLists are equal. i.e. both nodes have exactly the same children, but in any order
* Any child nodes that are in the ignoreContentsOfNodes array will be considered equal as long they are present in both.
* The contents of nodes in ignoreNodes can be different
* @param srcNode
* @param destNode
* @param ignoreContentsOfNodes
* @param ignoreAttrs
* @return
*/
private static boolean areIndividualNodeEqual(Node srcNode, Node destNode, String[] ignoreContentsOfNodes, String[] ignoreAttrs){
if(null != srcNode && null !=destNode){
//try normal node compare first. Takes all the effort out if this check passes, but we will fail this
//test a lot of the time.
if(srcNode.isEqualNode(destNode)){
return true;
}
//verify that the node types are the same
if(srcNode.getNodeType() == destNode.getNodeType()){
String srcNodeName = srcNode.getNodeName();
String srcLocalName = srcNode.getLocalName();
String srcNamespaceURI = srcNode.getNamespaceURI();
String srcPrefix = srcNode.getPrefix();
String srcNodeValue = srcNode.getNodeValue();
String destNodeName = destNode.getNodeName();
String destLocalName = destNode.getLocalName();
String destNamespaceURI = destNode.getNamespaceURI();
String destPrefix = destNode.getPrefix();
String destNodeValue = destNode.getNodeValue();
//verify that the node properties are all the same
if(StringUtil.equals(srcNodeName, destNodeName)
&& StringUtil.equals(srcLocalName, destLocalName)
&& StringUtil.equals(srcNamespaceURI, destNamespaceURI)
&& StringUtil.equals(srcPrefix, destPrefix)
&& StringUtil.equals(srcNodeValue, destNodeValue)
){
//if these are element Nodes check their attributes (only element nodes have attributes).
if(srcNode instanceof Element && destNode instanceof Element){
//if this is one of our ignore nodes we pass automatically. Otherwise we pass to compare attributes
//to see if the attributes match given our array of ignore attributes
if((Arrays.binarySearch(ignoreContentsOfNodes, srcNodeName) >= 0
|| compareAttributes((Element)srcNode, (Element)destNode, ignoreAttrs))){
//we have passed the attributes test.
return true;
}
}
else{
//if the nodes are not elements, then they do not have attributes to check, so they must be equal
return true;
}
}
}
}
return false;
}
/**
* This method takes a node and removes any empty text node children from it.
* Empty text nodes are defined as text nodes containing a only spaces,
* \n characters, \r characters and \t characters.
* You should normalize the node before calling this method on the off chance
* it will trim important spaces from text.
* @param node
*/
private static void removeEmptyTextNodes(Node node) {
NodeList nodeList = node.getChildNodes();
int length = nodeList.getLength();
for(int i=length-1; i>=0; i--) {
Node child = nodeList.item(i);
if(child.getNodeType()==Node.TEXT_NODE) {
Text txt = (Text) child;
String data = txt.getData();
if(StringUtil.isSpace(data)) {
node.removeChild(child);
}
} else {
removeEmptyTextNodes(child);
}
}
}
/**
* Exhaustive comparison of two documents. Does not assume child node order is the same in sourceDocument and destinationDocument.
* Ignores formatting text nodes (carriage returns, whitespace, tabs etc...)
* @param sourceDocument
* @param destinationDocument
* @return true if the contents of the two documents are considered equal, false otherwise
*/
public static boolean compare(Document sourceDocument, Document destinationDocument){
if(null != sourceDocument && null != destinationDocument){
Element sourceDocElement = sourceDocument.getDocumentElement();
Element destinationDocElement = destinationDocument.getDocumentElement();
if(null != sourceDocElement && null != destinationDocElement){
//before we do any comparing, normalize the nodes to remove any formatting text nodes, that might affect the compare
sourceDocElement.normalize();
removeEmptyTextNodes(sourceDocElement);
destinationDocElement.normalize();
removeEmptyTextNodes(destinationDocElement);
deepCompareElements(sourceDocElement, destinationDocElement, null, null, null);
}
}
return false;
}
/**
* Exhaustive comparison of two documents. Does not assume child node order is the same in sourceDocument and destinationDocument.
* Ignores formatting text nodes (carriage returns, whitespace, tabs etc...) sourceDocument and destinationDocument are required.
* Optionally, you can also provide an array of nodes to ignore, an array of nodes whose contents should be ignored and/or an
* array of attributes whose values should be ignored. Any or all of the optional arguments can be provided.
* @param sourceDocument
* @param destinationDocument
* @param ignoreContentsOfNodes - an array of nodes that only need to be present. Their children do not take part in the comparison.
* @param ignoreExistenceOfNodes - an array of nodes that will be ignored. If they are present in one element and not in the other, the two elements are still considered equal
* @param ignoreAttributes - an array of attributes to ignore. The "value" of these attributes will be ignored when comparing any node or child node.
* @return
*/
public static boolean compare(Document sourceDocument, Document destinationDocument, String[] ignoreContentsOfNodes, String[] ignoreExistenceOfNodes, String[] ignoreAttributes){
if(null != sourceDocument && null != destinationDocument){
Element sourceDocElement = sourceDocument.getDocumentElement();
Element destinationDocElement = destinationDocument.getDocumentElement();
if(null != sourceDocElement && null != destinationDocElement){
//before we do any comparing, normalize the nodes to remove any formatting text nodes, that might affect the compare
sourceDocElement.normalize();
removeEmptyTextNodes(sourceDocElement);
destinationDocElement.normalize();
removeEmptyTextNodes(destinationDocElement);
deepCompareElements(sourceDocElement, destinationDocElement, ignoreContentsOfNodes, ignoreExistenceOfNodes, ignoreAttributes);
}
}
return false;
}
/**
* Exhaustive comparison of two elements. Does not assume child node order is the same in sourceElement and destinationElement.
* Ignores formatting text nodes (carriage returns, whitespace, tabs etc...)
* @param sourceElement
* @param destinationElement
* @return true if the contents of the two elements are considered equal, false otherwise
*/
public static boolean compare(Element sourceElement, Element destinationElement){
if(null != sourceElement && null != destinationElement){
//before we do any comparing, normalize the nodes to remove any formatting text nodes, that might affect the compare
sourceElement.normalize();
removeEmptyTextNodes(sourceElement);
destinationElement.normalize();
removeEmptyTextNodes(destinationElement);
deepCompareElements(sourceElement, destinationElement, null, null, null);
}
return false;
}
/**
* Exhaustive comparison of two elements. Does not assume child node order is the same in sourceElement and destinationElement.
* Ignores formatting text nodes (carriage returns, whitespace, tabs etc...) sourceElement and destinationElement are required.
* Optionally, you can also provide an array of nodes to ignore, an array of nodes whose contents should be ignored and/or an
* array of attributes whose values should be ignored. Any or all of the optional arguments can be provided.
* @param sourceElement
* @param destinationElement
* @param ignoreContentsOfNodes - an array of nodes that only need to be present. Their children do not take part in the comparison.
* @param ignoreExistenceOfNodes - an array of nodes that will be ignored. If they are present in one element and not in the other, the two elements are still considered equal
* @param ignoreAttributes - an array of attributes to ignore. The "value" of these attributes will be ignored when comparing any node or child node.
* @return
*/
public static boolean compare(Element sourceElement, Element destinationElement, String[] ignoreContentsOfNodes, String[] ignoreExistenceOfNodes, String[] ignoreAttributes){
if(null != sourceElement && null != destinationElement){
//before we do any comparing, normalize the nodes to remove any formatting text nodes, that might affect the compare
sourceElement.normalize();
removeEmptyTextNodes(sourceElement);
destinationElement.normalize();
removeEmptyTextNodes(destinationElement);
deepCompareElements(sourceElement, destinationElement, ignoreContentsOfNodes, ignoreExistenceOfNodes, ignoreAttributes);
}
return false;
}
}