IT story

Java에서 2 개의 XML 문서를 비교하는 가장 좋은 방법

hot-time 2020. 5. 12. 08:00
반응형

Java에서 2 개의 XML 문서를 비교하는 가장 좋은 방법


기본적으로 사용자 정의 메시지 형식을 XML 메시지로 변환하여 다른 쪽 끝으로 보내는 응용 프로그램의 자동화 된 테스트를 작성하려고합니다. 입력 / 출력 메시지 쌍이 잘 갖추어져 있으므로 입력 메시지를 보내고 XML 메시지를 듣고 다른 쪽 끝을 듣기 만하면됩니다.

실제 출력을 예상 출력과 비교할 때가되었지만 몇 가지 문제가 있습니다. 내 첫 번째 생각은 예상 된 메시지와 실제 메시지를 문자열 비교하는 것입니다. 우리가 가지고있는 예제 데이터가 항상 일관되게 형식화되지는 않으며 XML 네임 스페이스에 다른 별칭이 사용되는 경우가 종종 있으며 때로는 네임 스페이스가 전혀 사용되지 않기 때문에 이것은 잘 작동하지 않습니다.

두 문자열을 구문 분석 한 다음 각 요소를 살펴보고 직접 비교할 수 있으며 그렇게하기가 어렵지 않지만 더 나은 방법이나 라이브러리를 활용할 수 있다는 느낌이 들었습니다.

따라서 삶은 문제는 다음과 같습니다.

둘 다 유효한 XML을 포함하는 두 개의 Java 문자열이 주어지면 의미 적으로 동등한 지 여부를 결정하는 방법은 무엇입니까? 차이점을 확인할 수있는 방법이 있다면 보너스 포인트입니다.


XMLUnit의 일처럼 들린다

예:

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

다음은 표준 JDK 라이브러리를 사용하여 문서가 동일한 지 확인합니다.

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

문서 doc1 = db.parse (새 파일 ( "file1.xml"));
doc1.normalizeDocument ();

문서 doc2 = db.parse (새 파일 ( "file2.xml"));
doc2.normalizeDocument ();

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

normalize () 사이클이 없는지 확인하기 위해 존재합니다 (기술적으로는 없을 것입니다)

위의 코드는 공백을 유지하고 평가하기 때문에 요소 내에서 공백이 동일해야합니다. Java와 함께 제공되는 표준 XML 파서는 표준 버전을 제공하는 기능을 설정하거나 xml:space이것이 문제가 될지를 이해하지 못 하므로 xerces와 같은 대체 XML 파서가 필요하거나 JDOM을 사용해야합니다.


Xom 에는 Canonicalizer 유틸리티가있어 DOM을 일반 형식으로 변환하여 문자열 화하고 비교할 수 있습니다. 따라서 공백의 불규칙성 또는 속성 순서에 관계없이 문서를 정기적으로 예측 가능한 비교할 수 있습니다.

이것은 Eclipse와 같은 전용 시각적 문자열 비교기가있는 IDE에서 특히 잘 작동합니다. 문서 간의 시맨틱 차이를 시각적으로 표시합니다.


최신 버전의 XMLUnit 은 두 XML을 동일하게 주장하는 작업에 도움이 될 수 있습니다. 또한 XMLUnit.setIgnoreWhitespace()XMLUnit.setIgnoreAttributeOrder()문제의 경우에 필요할 수 있습니다.

아래의 XML 단위 사용에 대한 간단한 예의 작업 코드를 참조하십시오.

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());
    }

}

Maven을 사용하는 경우 이것을 다음에 추가하십시오 pom.xml.

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

고마워, 나는 이것을 확장, 이것을 시도 ...

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();
    }
}

바탕 의 대답은 여기에 위해 XMLUnit의 V2를 사용하는 예제입니다.

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

테스트 코드는 다음과 같습니다.

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

이를 설명하는 문서는 https://github.com/xmlunit/xmlunit#comparing-two-documents입니다.


skaffman이 좋은 대답을하고있는 것 같습니다.

다른 방법은 xmlstarlet ( http://xmlstar.sourceforge.net/ )과 같은 명령 줄 유틸리티를 사용하여 XML을 포맷 한 다음 두 문자열을 모두 포맷 한 다음 diff 유틸리티 (라이브러리)를 사용하여 결과 출력 파일을 비교하는 것입니다. 네임 스페이스에 문제가있을 때 이것이 좋은 해결책인지는 모르겠습니다.


AssertJ 1.4+에는 XML 내용을 비교하기위한 특정 어설 션이 있습니다.

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

여기 문서가 있습니다


내가 사용하고 Altova DiffDog 구조적 (문자열 데이터를 무시) XML 파일을 비교하는 옵션이 있습니다.

이는 ( '텍스트 무시'옵션을 선택한 경우) :

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

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

그들이 구조적 평등을 가지고 있다는 점에서 평등합니다. 데이터가 다르지만 구조가 아닌 예제 파일이있는 경우 편리합니다!


이것은 전체 문자열 XML을 비교합니다 (가는 길에 다시 포맷). 클릭하면 XML 파일의 차이점을 시각적으로 볼 수 있으므로 IDE (IntelliJ, Eclipse)와 함께 쉽게 작업 할 수 있습니다.

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);
    }
}

클라이언트 코드 (테스트 코드)가 더 깨끗하기 때문에 이것을 XmlUnit보다 선호합니다.


아래 코드는 저에게 효과적입니다.

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

Java 애플리케이션에서 JExamXML 사용

    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 ) );

주요 질문에서 요청한 것과 동일한 기능이 필요했습니다. 타사 라이브러리를 사용할 수 없으므로 @Archimedes Trajano 솔루션을 기반으로 자체 솔루션을 만들었습니다.

다음은 내 해결책입니다.

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));
            }
        }
    }

}

두 개의 XML 문자열을 비교하고 일치하지 않는 네임 스페이스 매핑을 두 입력 문자열의 고유 한 값으로 변환하여 처리합니다.

네임 스페이스를 번역 할 때와 같이 미세 조정될 수 있습니다. 그러나 내 요구 사항에 따라 작업이 수행됩니다.


"의미 적으로 동등한"이라고 말했기 때문에 XML 출력이 (문자열) 같은지, 문자 그대로 같은 것을 원한다는 것을 문자 그대로 확인하는 것 이상의 것을 원한다고 가정합니다.

<foo> 여기 몇 가지 </ foo> </ code>

<foo> 여기 물건 </ foo> </ code>

동등한 것으로 읽습니다. 궁극적으로 메시지를 재구성하는 객체에 대해 "의미 적으로 동등한"방법을 정의하는 것이 중요합니다. 메시지에서 해당 객체를 빌드하고 원하는 equals ()를 사용하여 원하는 것을 정의하십시오.

참고 URL : https://stackoverflow.com/questions/141993/best-way-to-compare-2-xml-documents-in-java

반응형