La mejor forma de comparar 2 documentos XML en Java

Intento escribir una prueba automatizada de una aplicación que básicamente traduce un formato de mensaje personalizado en un mensaje XML y lo envía por el otro extremo. Tengo un buen conjunto de pares de mensajes de entrada / salida, así que todo lo que necesito hacer es enviar los mensajes de entrada y escuchar que el mensaje XML salga por el otro extremo.

Cuando llega el momento de comparar la salida real con la salida esperada, tengo algunos problemas. Mi primer pensamiento fue solo hacer comparaciones de cadenas en los mensajes esperados y reales. Esto no funciona muy bien porque los datos de ejemplo que tenemos no siempre están formateados de manera consistente y muchas veces hay alias diferentes para el espacio de nombres XML (y en ocasiones los espacios de nombres no se usan en absoluto).

Sé que puedo analizar ambas cadenas y luego recorrer cada elemento y compararlos yo mismo, y esto no sería demasiado difícil de hacer, pero tengo la sensación de que hay una mejor manera o una biblioteca que podría aprovechar.

Entonces, hervida, la pregunta es:

Dadas dos cadenas de Java que contienen XML válido, ¿cómo se determinaría si son semánticamente equivalentes? Puntos de bonificación si tiene una forma de determinar cuáles son las diferencias.

Suena como un trabajo para XMLUnit

Ejemplo:

public class SomeTest extends XMLTestCase { @Test public void test() { String xml1 = ... String xml2 = ... XMLUnit.setIgnoreWhitespace(true); // ignore whitespace differences // can also compare xml Documents, InputSources, Readers, Diffs assertXMLEquals(xml1, xml2); // assertXMLEquals comes from XMLTestCase } } 

Lo siguiente verificará si los documentos son iguales utilizando las bibliotecas JDK estándar.

 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance ();
 dbf.setNamespaceAware (verdadero);
 dbf.setCoalescing (true);
 dbf.setIgnoringElementContentWhitespace (true);
 dbf.setIgnoringComments (true);
 DocumentBuilder db = dbf.newDocumentBuilder ();

 Documento doc1 = db.parse (nuevo archivo ("archivo1.xml"));
 doc1.normalizeDocument ();

 Documento doc2 = db.parse (nuevo archivo ("archivo2.xml"));
 doc2.normalizeDocument ();

 Assert.assertTrue (doc1.isEqualNode (doc2));

normalize () está ahí para asegurarse de que no haya ciclos (técnicamente no habría ninguno)

El código anterior requerirá que los espacios en blanco sean los mismos dentro de los elementos, porque lo conserva y lo evalúa. El analizador XML estándar que viene con Java no le permite establecer una característica para proporcionar una versión canónica o comprender el xml:space si eso va a ser un problema, entonces puede necesitar un analizador XML de reemplazo como xerces o usar JDOM.

Xom tiene una utilidad Canonicalizer que convierte sus DOM en una forma regular, que luego puede stringify y comparar. Por lo tanto, independientemente de las irregularidades en el espacio en blanco o del orden de los atributos, puede obtener comparaciones regulares y predecibles de sus documentos.

Esto funciona especialmente bien en IDEs que tienen comparadores de cadenas visuales dedicados, como Eclipse. Obtienes una representación visual de las diferencias semánticas entre los documentos.

La última versión de XMLUnit puede ayudar a que el trabajo de afirmar dos XML sea igual. También XMLUnit.setIgnoreWhitespace() y XMLUnit.setIgnoreAttributeOrder() pueden ser necesarios para el caso en cuestión.

Vea el código de trabajo de un ejemplo simple del uso de la Unidad XML a continuación.

 import org.custommonkey.xmlunit.DetailedDiff; import org.custommonkey.xmlunit.XMLUnit; import org.junit.Assert; public class TestXml { public static void main(String[] args) throws Exception { String result = " "; // will be ok assertXMLEquals("", result); } public static void assertXMLEquals(String expectedXML, String actualXML) throws Exception { XMLUnit.setIgnoreWhitespace(true); XMLUnit.setIgnoreAttributeOrder(true); DetailedDiff diff = new DetailedDiff(XMLUnit.compareXML(expectedXML, actualXML)); List< ?> allDifferences = diff.getAllDifferences(); Assert.assertEquals("Differences found: "+ diff.toString(), 0, allDifferences.size()); } } 

Si usa Maven, agregue esto a su pom.xml :

  xmlunit xmlunit 1.4  

Gracias, extendí esto, prueba esto …

 import java.io.ByteArrayInputStream; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; public class XmlDiff { private boolean nodeTypeDiff = true; private boolean nodeValueDiff = true; public boolean diff( String xml1, String xml2, List diffs ) throws Exception { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); dbf.setCoalescing(true); dbf.setIgnoringElementContentWhitespace(true); dbf.setIgnoringComments(true); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc1 = db.parse(new ByteArrayInputStream(xml1.getBytes())); Document doc2 = db.parse(new ByteArrayInputStream(xml2.getBytes())); doc1.normalizeDocument(); doc2.normalizeDocument(); return diff( doc1, doc2, diffs ); } /** * Diff 2 nodes and put the diffs in the list */ public boolean diff( Node node1, Node node2, List diffs ) throws Exception { if( diffNodeExists( node1, node2, diffs ) ) { return true; } if( nodeTypeDiff ) { diffNodeType(node1, node2, diffs ); } if( nodeValueDiff ) { diffNodeValue(node1, node2, diffs ); } System.out.println(node1.getNodeName() + "/" + node2.getNodeName()); diffAttributes( node1, node2, diffs ); diffNodes( node1, node2, diffs ); return diffs.size() > 0; } /** * Diff the nodes */ public boolean diffNodes( Node node1, Node node2, List diffs ) throws Exception { //Sort by Name Map children1 = new LinkedHashMap(); for( Node child1 = node1.getFirstChild(); child1 != null; child1 = child1.getNextSibling() ) { children1.put( child1.getNodeName(), child1 ); } //Sort by Name Map children2 = new LinkedHashMap(); for( Node child2 = node2.getFirstChild(); child2!= null; child2 = child2.getNextSibling() ) { children2.put( child2.getNodeName(), child2 ); } //Diff all the children1 for( Node child1 : children1.values() ) { Node child2 = children2.remove( child1.getNodeName() ); diff( child1, child2, diffs ); } //Diff all the children2 left over for( Node child2 : children2.values() ) { Node child1 = children1.get( child2.getNodeName() ); diff( child1, child2, diffs ); } return diffs.size() > 0; } /** * Diff the nodes */ public boolean diffAttributes( Node node1, Node node2, List diffs ) throws Exception { //Sort by Name NamedNodeMap nodeMap1 = node1.getAttributes(); Map attributes1 = new LinkedHashMap(); for( int index = 0; nodeMap1 != null && index < nodeMap1.getLength(); index++ ) { attributes1.put( nodeMap1.item(index).getNodeName(), nodeMap1.item(index) ); } //Sort by Name NamedNodeMap nodeMap2 = node2.getAttributes(); Map attributes2 = new LinkedHashMap(); for( int index = 0; nodeMap2 != null && index < nodeMap2.getLength(); index++ ) { attributes2.put( nodeMap2.item(index).getNodeName(), nodeMap2.item(index) ); } //Diff all the attributes1 for( Node attribute1 : attributes1.values() ) { Node attribute2 = attributes2.remove( attribute1.getNodeName() ); diff( attribute1, attribute2, diffs ); } //Diff all the attributes2 left over for( Node attribute2 : attributes2.values() ) { Node attribute1 = attributes1.get( attribute2.getNodeName() ); diff( attribute1, attribute2, diffs ); } return diffs.size() > 0; } /** * Check that the nodes exist */ public boolean diffNodeExists( Node node1, Node node2, List diffs ) throws Exception { if( node1 == null && node2 == null ) { diffs.add( getPath(node2) + ":node " + node1 + "!=" + node2 + "\n" ); return true; } if( node1 == null && node2 != null ) { diffs.add( getPath(node2) + ":node " + node1 + "!=" + node2.getNodeName() ); return true; } if( node1 != null && node2 == null ) { diffs.add( getPath(node1) + ":node " + node1.getNodeName() + "!=" + node2 ); return true; } return false; } /** * Diff the Node Type */ public boolean diffNodeType( Node node1, Node node2, List diffs ) throws Exception { if( node1.getNodeType() != node2.getNodeType() ) { diffs.add( getPath(node1) + ":type " + node1.getNodeType() + "!=" + node2.getNodeType() ); return true; } return false; } /** * Diff the Node Value */ public boolean diffNodeValue( Node node1, Node node2, List diffs ) throws Exception { if( node1.getNodeValue() == null && node2.getNodeValue() == null ) { return false; } if( node1.getNodeValue() == null && node2.getNodeValue() != null ) { diffs.add( getPath(node1) + ":type " + node1 + "!=" + node2.getNodeValue() ); return true; } if( node1.getNodeValue() != null && node2.getNodeValue() == null ) { diffs.add( getPath(node1) + ":type " + node1.getNodeValue() + "!=" + node2 ); return true; } if( !node1.getNodeValue().equals( node2.getNodeValue() ) ) { diffs.add( getPath(node1) + ":type " + node1.getNodeValue() + "!=" + node2.getNodeValue() ); return true; } return false; } /** * Get the node path */ public String getPath( Node node ) { StringBuilder path = new StringBuilder(); do { path.insert(0, node.getNodeName() ); path.insert( 0, "/" ); } while( ( node = node.getParentNode() ) != null ); return path.toString(); } } 

Sobre la base de la respuesta de Tom , aquí hay un ejemplo usando XMLUnit v2.

Utiliza estas dependencias maven

   org.xmlunit xmlunit-core 2.0.0 test   org.xmlunit xmlunit-matchers 2.0.0 test  

..y aquí está el código de prueba

 import static org.junit.Assert.assertThat; import static org.xmlunit.matchers.CompareMatcher.isIdenticalTo; import org.xmlunit.builder.Input; import org.xmlunit.input.WhitespaceStrippedSource; public class SomeTest extends XMLTestCase { @Test public void test() { String result = ""; String expected = " "; // ignore whitespace differences // https://github.com/xmlunit/user-guide/wiki/Providing-Input-to-XMLUnit#whitespacestrippedsource assertThat(result, isIdenticalTo(new WhitespaceStrippedSource(Input.from(expected).build()))); assertThat(result, isIdenticalTo(Input.from(expected).build())); // will fail due to whitespace differences } } 

La documentación que describe esto es https://github.com/xmlunit/xmlunit#comparing-two-documents

skaffman parece estar dando una buena respuesta.

Otra forma es, probablemente, formatear el XML utilizando una utilidad de línea de comunicación como xmlstarlet ( http://xmlstar.sourceforge.net/ ) y luego formatee ambas cadenas y luego use cualquier utilidad de diferencias (biblioteca) para diferenciar los archivos de salida resultantes. No sé si esta es una buena solución cuando hay problemas con los espacios de nombres.

Estoy usando Altova DiffDog, que tiene opciones para comparar archivos XML estructuralmente (ignorando los datos de cadena).

Esto significa que (si se marca la opción ‘ignorar texto’):

 xxx 

y

 yyy 

son iguales en el sentido de que tienen igualdad estructural. Esto es útil si tiene archivos de ejemplo que difieren en los datos, ¡pero no en la estructura!

Esto comparará los XML completos de cadenas (formateándolos en el camino). Hace que sea fácil trabajar con su IDE (IntelliJ, Eclipse), ya que simplemente hace clic y visualiza visualmente la diferencia en los archivos XML.

 import org.apache.xml.security.c14n.CanonicalizationException; import org.apache.xml.security.c14n.Canonicalizer; import org.apache.xml.security.c14n.InvalidCanonicalizerException; import org.w3c.dom.Element; import org.w3c.dom.bootstrap.DOMImplementationRegistry; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSSerializer; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import java.io.IOException; import java.io.StringReader; import static org.apache.xml.security.Init.init; import static org.junit.Assert.assertEquals; public class XmlUtils { static { init(); } public static String toCanonicalXml(String xml) throws InvalidCanonicalizerException, ParserConfigurationException, SAXException, CanonicalizationException, IOException { Canonicalizer canon = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS); byte canonXmlBytes[] = canon.canonicalize(xml.getBytes()); return new String(canonXmlBytes); } public static String prettyFormat(String input) throws TransformerException, ParserConfigurationException, IOException, SAXException, InstantiationException, IllegalAccessException, ClassNotFoundException { InputSource src = new InputSource(new StringReader(input)); Element document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src).getDocumentElement(); Boolean keepDeclaration = input.startsWith("< ?xml"); DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS"); LSSerializer writer = impl.createLSSerializer(); writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); writer.getDomConfig().setParameter("xml-declaration", keepDeclaration); return writer.writeToString(document); } public static void assertXMLEqual(String expected, String actual) throws ParserConfigurationException, IOException, SAXException, CanonicalizationException, InvalidCanonicalizerException, TransformerException, IllegalAccessException, ClassNotFoundException, InstantiationException { String canonicalExpected = prettyFormat(toCanonicalXml(expected)); String canonicalActual = prettyFormat(toCanonicalXml(actual)); assertEquals(canonicalExpected, canonicalActual); } } 

Prefiero esto a XmlUnit porque el código del cliente (código de prueba) es más limpio.

AssertJ 1.4+ tiene aserciones específicas para comparar contenido XML:

 String expectedXml = ""; String actualXml = ""; assertThat(actualXml).isXmlEqualTo(expectedXml); 

Aquí está la documentación

Usando JExamXML con la aplicación Java

  import com.a7soft.examxml.ExamXML; import com.a7soft.examxml.Options; ................. // Reads two XML files into two strings String s1 = readFile("orders1.xml"); String s2 = readFile("orders.xml"); // Loads options saved in a property file Options.loadOptions("options"); // Compares two Strings representing XML entities System.out.println( ExamXML.compareXMLString( s1, s2 ) ); 

Requirí la misma funcionalidad que solicité en la pregunta principal. Como no tenía permitido utilizar ninguna biblioteca de terceros, he creado mi propia solución basándose en la solución de @Archimedes Trajano.

La siguiente es mi solución.

 import java.io.ByteArrayInputStream; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.junit.Assert; import org.w3c.dom.Document; /** * Asserts for asserting XML strings. */ public final class AssertXml { private AssertXml() { } private static Pattern NAMESPACE_PATTERN = Pattern.compile("xmlns:(ns\\d+)=\"(.*?)\""); /** * Asserts that two XML are of identical content (namespace aliases are ignored). * * @param expectedXml expected XML * @param actualXml actual XML * @throws Exception thrown if XML parsing fails */ public static void assertEqualXmls(String expectedXml, String actualXml) throws Exception { // Find all namespace mappings Map fullnamespace2newAlias = new HashMap(); generateNewAliasesForNamespacesFromXml(expectedXml, fullnamespace2newAlias); generateNewAliasesForNamespacesFromXml(actualXml, fullnamespace2newAlias); for (Entry entry : fullnamespace2newAlias.entrySet()) { String newAlias = entry.getValue(); String namespace = entry.getKey(); Pattern nsReplacePattern = Pattern.compile("xmlns:(ns\\d+)=\"" + namespace + "\""); expectedXml = transletaNamespaceAliasesToNewAlias(expectedXml, newAlias, nsReplacePattern); actualXml = transletaNamespaceAliasesToNewAlias(actualXml, newAlias, nsReplacePattern); } // nomralize namespaces accoring to given mapping DocumentBuilder db = initDocumentParserFactory(); Document expectedDocuemnt = db.parse(new ByteArrayInputStream(expectedXml.getBytes(Charset.forName("UTF-8")))); expectedDocuemnt.normalizeDocument(); Document actualDocument = db.parse(new ByteArrayInputStream(actualXml.getBytes(Charset.forName("UTF-8")))); actualDocument.normalizeDocument(); if (!expectedDocuemnt.isEqualNode(actualDocument)) { Assert.assertEquals(expectedXml, actualXml); //just to better visualize the diffeences ie in eclipse } } private static DocumentBuilder initDocumentParserFactory() throws ParserConfigurationException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(false); dbf.setCoalescing(true); dbf.setIgnoringElementContentWhitespace(true); dbf.setIgnoringComments(true); DocumentBuilder db = dbf.newDocumentBuilder(); return db; } private static String transletaNamespaceAliasesToNewAlias(String xml, String newAlias, Pattern namespacePattern) { Matcher nsMatcherExp = namespacePattern.matcher(xml); if (nsMatcherExp.find()) { xml = xml.replaceAll(nsMatcherExp.group(1) + "[:]", newAlias + ":"); xml = xml.replaceAll(nsMatcherExp.group(1) + "=", newAlias + "="); } return xml; } private static void generateNewAliasesForNamespacesFromXml(String xml, Map fullnamespace2newAlias) { Matcher nsMatcher = NAMESPACE_PATTERN.matcher(xml); while (nsMatcher.find()) { if (!fullnamespace2newAlias.containsKey(nsMatcher.group(2))) { fullnamespace2newAlias.put(nsMatcher.group(2), "nsTr" + (fullnamespace2newAlias.size() + 1)); } } } } 

Compara dos cadenas XML y se ocupa de las asignaciones de espacios de nombres que no coinciden traduciéndolas a valores únicos en ambas cadenas de entrada.

Se puede ajustar, por ejemplo, en caso de traducción de espacios de nombres. Pero para mis requisitos solo hace el trabajo.

El código siguiente funciona para mí

 String xml1 = ... String xml2 = ... XMLUnit.setIgnoreWhitespace(true); XMLUnit.setIgnoreAttributeOrder(true); XMLAssert.assertXMLEqual(actualxml, xmlInDb); 

Como dice “semánticamente equivalente”, supongo que quiere decir que quiere hacer algo más que verificar literalmente que las salidas xml son (cadenas) iguales, y que querría algo así como

algunas cosas aquí

y

algunas cosas aquí

lee como equivalente. En última instancia, va a importar cómo está definiendo “semánticamente equivalente” en cualquier objeto desde el que esté reconstituyendo el mensaje. Simplemente cree ese objeto a partir de los mensajes y use un valor igual personalizado () para definir lo que está buscando.