web-dev-qa-db-fra.com

Meilleure façon de comparer 2 documents XML dans 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, donc tout ce que j'ai à faire est d'envoyer les messages d'entrée et d'écouter le message XML à l'autre bout.

Quand vient le temps de comparer la sortie réelle à la sortie attendue, je rencontre quelques problèmes. Ma première pensée a été 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 y a souvent des alias différents utilisés pour l'espace de noms XML (et parfois, les espaces de noms ne sont pas utilisés du tout.)

Je sais que je peux analyser les deux chaînes, puis parcourir chaque élément et les comparer moi-même. Cela ne sera pas trop difficile à faire, mais j'ai l'impression qu'il existe une meilleure façon ou une bibliothèque que je pourrais exploiter.

Donc, résumée, la question est:

Étant donné deux chaînes Java qui contiennent toutes les deux un code XML valide, comment procéderiez-vous pour déterminer si elles sont sémantiquement équivalentes? Points bonus si vous avez un moyen de déterminer quelles sont les différences.

193
Mike Deck

Cela ressemble à un travail pour XMLUnit

Exemple:

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
  }
}
191
Tom

Les éléments suivants vérifieront si les documents sont identiques à l'aide des 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 (techniquement, il n'y en aurait pas)

Le code ci-dessus nécessitera cependant que les espaces soient identiques, car il les conserve et les évalue. L'analyseur XML standard fourni avec Java ne vous permet pas de définir une fonctionnalité fournissant une version canonique ou de comprendre xml:space si cela pose problème, vous aurez peut-être besoin d'un analyseur XML de remplacement. tels que xerces ou utiliser JDOM.

35
Archimedes Trajano

Xom a un utilitaire Canonicalizer qui transforme vos DOM en une forme régulière, que vous pouvez ensuite stringifier et comparer. Ainsi, indépendamment des irrégularités d’espace ou du classement des attributs, vous pouvez obtenir des comparaisons régulières et prévisibles de vos documents.

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

28
skaffman

La dernière version de XMLUnit peut aider le travail d'affirmation que deux XML sont égaux. De plus, XMLUnit.setIgnoreWhitespace() et XMLUnit.setIgnoreAttributeOrder() peuvent être nécessaires dans le cas en question.

Voir le code de travail d'un exemple simple d'utilisation d'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(String[] args) throws Exception {
        String result = "<abc             attr=\"value1\"                title=\"something\">            </abc>";
        // will be ok
        assertXMLEquals("<abc attr=\"value1\" title=\"something\"></abc>", 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 vous utilisez Maven, ajoutez ceci à votre pom.xml:

<dependency>
    <groupId>xmlunit</groupId>
    <artifactId>xmlunit</artifactId>
    <version>1.4</version>
</dependency>
20
acdcjunior

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( String xml1, String xml2, List<String> 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<String> 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<String> diffs ) throws Exception
    {
        //Sort by Name
        Map<String,Node> children1 = new LinkedHashMap<String,Node>();      
        for( Node child1 = node1.getFirstChild(); child1 != null; child1 = child1.getNextSibling() )
        {
            children1.put( child1.getNodeName(), child1 );
        }

        //Sort by Name
        Map<String,Node> children2 = new LinkedHashMap<String,Node>();      
        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<String> diffs ) throws Exception
    {        
        //Sort by Name
        NamedNodeMap nodeMap1 = node1.getAttributes();
        Map<String,Node> attributes1 = new LinkedHashMap<String,Node>();        
        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<String,Node> attributes2 = new LinkedHashMap<String,Node>();        
        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<String> 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<String> 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<String> 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();
    }
}
6
Javelin

En s'appuyant sur la réponse de Tom , voici un exemple utilisant XMLUnit v2.

Il utilise ces dépendances maven

    <dependency>
        <groupId>org.xmlunit</groupId>
        <artifactId>xmlunit-core</artifactId>
        <version>2.0.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.xmlunit</groupId>
        <artifactId>xmlunit-matchers</artifactId>
        <version>2.0.0</version>
        <scope>test</scope>
    </dependency>

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

public class SomeTest extends XMLTestCase {
    @Test
    public void test() {
        String result = "<root></root>";
        String expected = "<root>  </root>";

        // 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 documentation qui décrit ceci est https://github.com/xmlunit/xmlunit#comparing-two-documents

6
Tom Saleeba

skaffman semble donner une bonne réponse.

une autre méthode consiste probablement à formater le fichier XML à l'aide d'un utilitaire de ligne de commande tel que xmlstarlet ( http://xmlstar.sourceforge.net/ ), puis à formater les deux chaînes, puis à utiliser un utilitaire de diff (bibliothèque) pour: diff les fichiers de sortie résultants. Je ne sais pas si c'est une bonne solution lorsque des problèmes se posent avec les espaces de noms.

3
anjanb

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

String expectedXml = "<foo />";
String actualXml = "<bar />";
assertThat(actualXml).isXmlEqualTo(expectedXml);

Voici le Documentation

3

J'utilise Altova DiffDog qui permet de comparer la structure de fichiers XML (en ignorant les données de chaîne).

Cela signifie que (si l'option "ignorer le texte" est cochée):

<foo a="xxx" b="xxx">xxx</foo>

et

<foo b="yyy" a="yyy">yyy</foo> 

sont égaux en ce sens qu'ils ont une égalité structurelle. C'est pratique si vous avez des exemples de fichiers dont les données diffèrent, mais pas la structure!

Le code ci-dessous fonctionne pour moi

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

Ceci comparera les chaînes XML complètes (en les reformatant en cours de route). Cela facilite le travail avec votre IDE (IntelliJ, Eclipse), car vous cliquez simplement sur la différence et la visualisez de manière visuelle 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.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);
    }
}

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

1
Wojtek

J'avais besoin de la même fonctionnalité que celle demandée dans la question principale. Comme je n'avais pas le droit d'utiliser de bibliothèques tierces, 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 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<String, String> fullnamespace2newAlias = new HashMap<String, String>();
        generateNewAliasesForNamespacesFromXml(expectedXml, fullnamespace2newAlias);
        generateNewAliasesForNamespacesFromXml(actualXml, fullnamespace2newAlias);

        for (Entry<String, String> 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 i.e. 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<String, String> 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 les mappages d'espaces de noms qui ne correspondent pas en les traduisant en valeurs uniques dans les deux chaînes d'entrée.

Peut être ajusté, c'est-à-dire en cas de traduction d'espaces de noms. Mais pour mes besoins fait juste le travail.

0
TouDick

Utilisation de JExamXML avec l'application 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 ) );
0
sreehari