在Java中比较2个XML文档的最佳方式
我试图编写一个应用程序的自动化testing,基本上将自定义消息格式转换为XML消息,并将其发送到另一端。 我有一组好的input/输出消息对,所以我需要做的就是发送input消息,并监听XML消息从另一端出来。
当需要将实际产出与预期产出进行比较时,我遇到了一些问题。 我的第一个想法就是对预期的和实际的消息进行string比较。 这样做效果不好,因为我们所拥有的示例数据并不总是格式一致,并且通常有不同的用于XML名称空间的别名(有时候根本不使用名称空间)。
我知道我可以parsing这两个string,然后遍历每个元素并自己比较,这不会太难,但是我感觉有更好的方法或者我可以利用的库。
所以,简单地说,问题是:
给定两个包含有效XML的Javastring,你将如何确定它们是否在语义上相同? 奖励积分,如果你有办法确定差异是什么。
听起来像是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(真); dbf.setCoalescing(真); dbf.setIgnoringElementContentWhitespace(真); dbf.setIgnoringComments(真); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc1 = db.parse(new File(“file1.xml”)); doc1.normalizeDocument(); Document doc2 = db.parse(new File(“file2.xml”)); doc2.normalizeDocument(); Assert.assertTrue(doc1.isEqualNode(DOC2));
normalize()在那里,以确保没有周期(技术上不会有任何)
上面的代码将要求元素中的空白是相同的,因为它保留并评估它。 Java附带的标准XMLparsing器不允许您设置提供规范版本的function或者理解xml:space
如果这将是一个问题,那么您可能需要更换XMLparsing器(如xerces)或使用JDOM。
Xom有一个Canonicalizer实用程序,可以将您的DOM转换为常规forms,然后您可以将其进行串联和比较。 因此,不pipe空白不规则或属性sorting,您都可以定期,可预测地比较文档。
在具有专用可视string比较器(如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(); } }
skaffman似乎在给出一个很好的答案。
另一种方法可能是使用像xmlstarlet( http://xmlstar.sourceforge.net/ )这样的命令行实用程序格式化XML,然后格式化这两个string,然后使用任何diff实用程序(库)来区分生成的输出文件。 在命名空间出现问题时,我不知道这是否是一个好的解决scheme。
基于Tom的回答,下面是一个使用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>
..这是testing代码
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
我正在使用Altova DiffDog ,它可以从结构上比较XML文件(忽略string数据)。
这意味着(如果选中“忽略文本”选项):
<foo a="xxx" b="xxx">xxx</foo>
和
<foo b="yyy" a="yyy">yyy</foo>
在结构平等的意义上是平等的。 如果您的示例文件在数据上有所不同,但不是结构,则此function非常方便!
这将比较完整的stringXML(重新格式化它们)。 它使您可以轻松地使用IDE(IntelliJ,Eclipse),只需点击鼠标,直观地看到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); } }
我更喜欢这个XmlUnit,因为客户端代码(testing代码)更清洁。
AssertJ 1.4+具有特定的断言来比较XML内容:
String expectedXml = "<foo />"; String actualXml = "<bar />"; assertThat(actualXml).isXmlEqualTo(expectedXml);
这里是文档
与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 ) );
我需要主要问题中所要求的相同function。 由于我没有被允许使用任何第三方库,我已经基于@Archimedes Trajano解决scheme创build了自己的解决scheme。
以下是我的解决scheme。
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 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<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)); } } } }
它比较两个XMLstring,并通过将它们转换为两个inputstring中的唯一值来处理任何不匹配的名称空间映射。
可以很好地调整,即在命名空间翻译的情况下。 但是我的要求只是做这项工作。
下面的代码适合我
String xml1 = ... String xml2 = ... XMLUnit.setIgnoreWhitespace(true); XMLUnit.setIgnoreAttributeOrder(true); XMLAssert.assertXMLEqual(actualxml, xmlInDb);
既然你说“语义等价”,我假设你的意思是,你想要做的不仅仅是从字面上validationxml输出是(string)等于,而且你想要的东西
<foo>这里有些东西</ foo> </ code>
和
<foo>这里有些东西</ foo> </ code>
请阅读等效。 最终,重要的是你如何定义“语义等价”的任何对象,你正在重构的消息。 只需从消息中构build该对象,然后使用自定义的equals()来定义要查找的内容。