Meilleur moyen de comparer 2 documents XML en Java

J’essaie d’écrire un test automatisé d’une application qui traduit essentiellement un format de message personnalisé en un message XML et l’envoie à l’autre extrémité. J’ai un bon ensemble de paires de messages d’entrée / sortie, il me suffit donc d’envoyer les messages d’entrée et d’écouter le message XML à l’autre bout.

Lorsque vient le temps de comparer la sortie réelle à la sortie attendue, je rencontre des problèmes. Ma première pensée était juste de faire des comparaisons de chaînes sur les messages attendus et réels. Cela ne fonctionne pas très bien car les exemples de données dont nous disposons ne sont pas toujours formatés de manière cohérente et il existe souvent des alias différents pour l’espace de noms XML (et parfois les espaces de noms ne sont pas utilisés du tout).

Je sais que je peux parsingr les deux cordes et ensuite passer en revue chaque élément et les comparer moi-même, ce qui ne serait pas trop difficile à faire, mais j’ai l’impression qu’il existe un meilleur moyen ou une bibliothèque.

Donc, cuit, la question est:

Étant donné que deux chaînes Java contiennent du code XML valide, comment procéder pour déterminer si elles sont sémantiquement équivalentes? Des points bonus si vous avez un moyen de déterminer les différences.

Sonne comme un travail pour XMLUnit

Exemple:

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

Les éléments suivants vérifient si les documents sont égaux en utilisant les bibliothèques JDK standard.

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

 Document doc1 = db.parse (nouveau fichier ("fichier1.xml"));
 doc1.normalizeDocument ();

 Document doc2 = db.parse (nouveau fichier ("fichier2.xml"));
 doc2.normalizeDocument ();

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

normalize () est là pour s’assurer qu’il n’y a pas de cycles (il n’y en a pas du tout techniquement)

Le code ci-dessus nécessitera toutefois que les espaces soient identiques dans les éléments, car il le conserve et l’évalue. L’parsingur XML standard fourni avec Java ne vous permet pas de définir une fonctionnalité pour fournir une version canonique ou comprendre xml:space si cela pose problème, vous devrez peut-être utiliser un parsingur XML de remplacement tel que xerces ou utiliser JDOM.

Xom a un utilitaire Canonicalizer qui transforme vos DOM en une forme régulière, que vous pouvez ensuite contraindre et comparer. Ainsi, quelles que soient les irrégularités d’espace ou le classement des atsortingbuts, vous pouvez obtenir des comparaisons régulières et prévisibles de vos documents.

Cela fonctionne particulièrement bien dans les IDE dotés de comparateurs de chaînes visuels dédiés, comme Eclipse. Vous obtenez une représentation visuelle des différences sémantiques entre les documents.

La dernière version de XMLUnit peut aider à faire valoir deux tâches XML. XMLUnit.setIgnoreWhitespace() et XMLUnit.setIgnoreAtsortingbuteOrder() peuvent également être nécessaires pour le cas en question.

Voir le code de travail d’un exemple simple d’utilisation de l’unité XML ci-dessous.

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

Si vous utilisez Maven, ajoutez-le à votre pom.xml :

  xmlunit xmlunit 1.4  

Merci, j’ai prolongé ceci, essayez ceci …

 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( Ssortingng xml1, Ssortingng 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()); diffAtsortingbutes( 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 diffAtsortingbutes( Node node1, Node node2, List diffs ) throws Exception { //Sort by Name NamedNodeMap nodeMap1 = node1.getAtsortingbutes(); Map atsortingbutes1 = 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 atsortingbutes2 = 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 Ssortingng getPath( Node node ) { SsortingngBuilder path = new SsortingngBuilder(); do { path.insert(0, node.getNodeName() ); path.insert( 0, "/" ); } while( ( node = node.getParentNode() ) != null ); return path.toSsortingng(); } } 

En s’appuyant sur la réponse de Tom , voici un exemple d’utilisation de XMLUnit v2.

Il utilise ces dépendances maven

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

..et voici le code de test

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

La documentation qui décrit ceci est https://github.com/xmlunit/xmlunit#comparing-two-documents

skaffman semble donner une bonne réponse.

Une autre façon consiste probablement à formater le XML en utilisant un utilitaire de ligne de commande tel que xmlstarlet ( http://xmlstar.sourceforge.net/ ), puis à formater les deux chaînes, puis à utiliser tout utilitaire diff (bibliothèque) pour différencier les fichiers de sortie obtenus. Je ne sais pas si c’est une bonne solution lorsque les problèmes concernent les espaces de noms.

J’utilise Altova DiffDog qui a des options pour comparer les fichiers XML de manière structurelle (en ignorant les données de chaîne).

Cela signifie que (si vous cochez l’option “ignorer le texte”):

 xxx 

et

 yyy 

sont égales dans le sens où elles ont une égalité structurelle. C’est pratique si vous avez des exemples de fichiers dont les données diffèrent, mais pas la structure!

Cela permettra de comparer les chaînes XML complètes (les reformater en cours de route). Il est facile de travailler avec votre IDE (IntelliJ, Eclipse), car il vous suffit de cliquer et de voir visuellement la différence dans les fichiers 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.SsortingngReader; import static org.apache.xml.security.Init.init; import static org.junit.Assert.assertEquals; public class XmlUtils { static { init(); } public static Ssortingng toCanonicalXml(Ssortingng 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 Ssortingng(canonXmlBytes); } public static Ssortingng prettyFormat(Ssortingng input) throws TransformerException, ParserConfigurationException, IOException, SAXException, InstantiationException, IllegalAccessException, ClassNotFoundException { InputSource src = new InputSource(new SsortingngReader(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); } } 

Je préfère cela à XmlUnit car le code client (code de test) est plus propre.

AssertJ 1.4+ a des assertions spécifiques pour comparer le contenu XML:

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

Voici la documentation

Utiliser JExamXML avec l’application Java

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

J’ai eu besoin des mêmes fonctionnalités que celles demandées dans la question principale. Comme je n’étais autorisé à utiliser aucune bibliothèque tierce, j’ai créé ma propre solution basée sur la solution @Archimedes Trajano.

Voici ma solution.

 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 ssortingngs. */ public final class AssertXml { private AssertXml() { } private static Pattern NAMESPACE_PATTERN = Pattern.comstack("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(Ssortingng expectedXml, Ssortingng actualXml) throws Exception { // Find all namespace mappings Map fullnamespace2newAlias = new HashMap(); generateNewAliasesForNamespacesFromXml(expectedXml, fullnamespace2newAlias); generateNewAliasesForNamespacesFromXml(actualXml, fullnamespace2newAlias); for (Entry entry : fullnamespace2newAlias.entrySet()) { Ssortingng newAlias = entry.getValue(); Ssortingng namespace = entry.getKey(); Pattern nsReplacePattern = Pattern.comstack("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 Ssortingng transletaNamespaceAliasesToNewAlias(Ssortingng xml, Ssortingng 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(Ssortingng 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)); } } } } 

Il compare deux chaînes XML et prend en charge tous les mappages d’espaces de noms incompatibles en les traduisant en valeurs uniques dans les deux chaînes d’entrée.

Peut être ajusté, par exemple en cas de traduction des espaces de noms. Mais pour mes besoins, il suffit de faire le travail.

Le code ci-dessous fonctionne pour moi

 Ssortingng xml1 = ... Ssortingng xml2 = ... XMLUnit.setIgnoreWhitespace(true); XMLUnit.setIgnoreAtsortingbuteOrder(true); XMLAssert.assertXMLEqual(actualxml, xmlInDb); 

Puisque vous dites “sémantiquement équivalent”, je suppose que vous voulez dire que vous voulez faire plus que vérifier littéralement que les sorties XML sont (chaîne) égales, et que vous voulez quelque chose comme

quelques trucs ici

et

quelques trucs ici

lis comme équivalent. En fin de compte, il importera que vous définissiez «sémantiquement équivalent» sur l’object à partir duquel vous reconstituez le message. Construisez simplement cet object à partir des messages et utilisez un égal à () pour définir ce que vous recherchez.