65.9K
CodeProject 正在变化。 阅读更多。
Home

EasyXML:实用的 XML 处理工具包

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2015 年 5 月 22 日

CPOL

9分钟阅读

viewsIcon

10601

downloadIcon

156

一个开源的 JAVA 库,可以从零开始或基于模板组合新的 XML 文档,并以简单高效的方式将 XML 解析为字典或 JSON。

引言

最近,我参与了一个基于 SOAP 服务的 JAVA 项目的 API 测试,这立即让我想起,我可以运用之前在 C# 中实现的一些技能/方法来比我的同事更有效地处理它,我尝试构建了一个框架来组合和解析 XML。然而,由于公司战略的一些变化,在工作了不到一周后,我不得不回归 .NET 领域。我不甘心放弃我的努力,于是我决定将我的设计转变为一个个人项目,并加入一些实用的功能,例如:

  1. 通过简化的组件和方法组合格式良好的 XML 文档
  2. 将 XML 内容转换为字符串 Map 列表
  3. 基于现有 XML 模板生成 XML 文档
  4. 将 XML 转换为 JSON

背景

基于 XML 的 SOAP 协议仍然广泛用于服务 API 的实现,我被指派测试一些使用 JAVA 开发的 SOAP 服务 API。我的公司倾向于通过编码而不是使用 SoapUI 来构建一个可维护且可扩展的测试套件,该套件包括通过 SQL 查询从数据库获取测试数据、相应地组合 SOAP 请求、接收相应的 SOAP 响应,最后将响应内容与测试数据中的预期值进行匹配。

因此,这里的挑战在于:

  • 如何将所有测试方法的统一格式 `Map<String, String>` 的测试数据应用于生成所需的 XML 文档格式的 SOAP 请求?
  • 如何从 SOAP 响应(也同样是 XML 文档)中提取有用的信息,并将其转换为 `Map<String, String>`,以便逐一与测试数据进行比较?

有了详细的请求样本,要弄清楚如何根据测试数据更改某些 XML 节点的值并不难,但用 JAVA 实现仍然很困难。从零开始组合一个 XML 文档或文档的一部分显然不容易:尤其是对于流行的 DOM/SAX 解析器,在修改任何节点之前,必须非常清楚如何构建 DOM 树中的节点链。

解析响应以获取包含响应数据的 Map,并且这些 Map 具有与测试数据相同的键,这可能更难:同样,必须非常小心地选择目标元素,并且当它们是数组时,将它们的转换为 Map 可能非常困难。即使我们有一些 Hibernate 类,可以通过解组响应来获取持久化对象,但找到沿着父节点/列表的正确属性对我来说仍然是一件痛苦的事情。

作为所有格式良好的 XML 文档,需要插入到请求中或从响应中提取的信息总是存储为属性值或子元素的内部文本,这些属性值或内部文本可以通过固定路径从根节点追溯。将**这些路径**与测试数据的键关联起来,并自动查找和保留**这些路径**下的所有属性和元素,这意味着可以使用一套代码来服务所有服务测试,就像您在 EasyXML 应用程序部分看到的。

关键概念

EasyXML 无意遵循 XML 规范,它旨在提供工具来处理 XML 文档作为信息载体,并且只关心包含大多数应用程序所需信息的组件。

  • Element 名称
  • ElementInnerText
  • Attribute 名称
  • Attribute
  • 最后,所有 ElementAttribute 的关系

Attribute 的名称/值很容易理解。与传统的 XML 解析器将 Element 的值定义为 null,并将 Element 标签之间的连续字符定义为 TextNode 不同,EasyXML 将一个 Element 的值定义为其中常规 TextNode 的串联(如果它们不为空)。因此,只有两个组件被定义为 ElementAttribute,它们都有 name 和 value 字段,可以映射到 Map<String, String>.EntryKeyValuePair

显然,具有相同名称的 Element 可以是 DOM 树不同层的,或者不同 Element 的不同 Attribute。然而,它们与根节点的相对位置总是固定的,这被称为“**路径**”。

ElementAttribute 的路径,类似于 Windows 命令控制台中显示的文件的路径,显示了如何从根节点沿着祖先树定位它们。它有两种形式,带有默认符号:

  • 通过连接 Element 名称并使用 > 来表示 Element 路径

    例如,root>body>message 表示 <message> 的 Element,其父节点是 <root> 下的 <*body*> 的 Element。

  • 通过连接 Element 路径和 Attribute 名称并使用 < 来表示 Attribute 路径

    例如,root>body>message<id 表示所有 <root><body><message> 的 Elementid 属性。

通常,理解这些就足以尝试使用 EasyXML 了。

Application

最新版本可在 GitHub.com 上找到。

轻松创建 XML

EasyXML 使创建复杂的 XML 文档变得高效。例如,对于下面列出的功能:

    @Test
    public void testDocument_composeSOAP() throws SAXException {
    
        soapDoc = (Document) new Document(SoapEnvelope)    
            .addAttribute("xmlns:SOAP",
                "http://schemas.xmlsoap.org/soap/envelope/")    
            .addAttribute("xmlns:SOAP_ENC",
                "http://schemas.xmlsoap.org/soap/encoding/")    
            .addChildElement(new Element(SoapHeader));
    
        // This is equal to Element soapBody = new Element(SoapBody, soapDoc);    
        soapDoc.addChildElement(new Element(SoapBody));
    
        // Create a new empty node under SoapBody    
        soapDoc.setValuesOf(SoapBody + ">SampleSoapRequest");
    
        // Set this new node as the default container for following operations.    
        soapDoc.setDefaultContainerByPath(SoapBody + ">SampleSoapRequest");
    
        // Now the path would be relative to the default container, and add
        // three User with ID/Password defined as their attributes    
        soapDoc.setValuesOf("Credential>User<ID", "test01", "test02", "test03");
    
        // Use null as placeholder, that won't set Password attribute to the
        // second user    
        soapDoc.setValuesOf("Credential>User<Password", "password01", null,
            "password03");
    
        soapDoc.setValuesOf("Credential>URL", "http:\\localhost:8080");
    
        // The existing element can also be used to append new Attribute    
        soapDoc.setValuesOf("Credential<ValidDays", "33");
    
        // Attributes would be allocated to different elements    
        soapDoc.setValuesOf("RequestData<ID", "item1038203", "s893hwkldja");    
        soapDoc.setValuesOf("RequestData<Date", null, "12-12-2005");
    
        // While text would be appended to the first matched container element
        // "RequestData"
    
        soapDoc.setValuesOf("RequestData>Text", "DSOdji 23 djusu8 adaad adssd",
            "Another text");
    
        System.out.println(soapDoc.toString());
    }

XML 文档可以这样生成:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP:Envelop xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" 
      xmlns:SOAP_ENC="http://schemas.xmlsoap.org/soap/encoding/">
    <SOAP:Header/>
    <SOAP:Body>
        <SampleSoapRequest>
            <Credential ValidDays="33">
                <User ID="test01" Password="password01"/>
                <User ID="test02"/>
                <User ID="test03" Password="password03"/>
                <URL>http:\localhost:8080</URL>
            </Credential>
            <RequestData ID="item1038203">
                <Text>DSOdji 23 djusu8 adaad adssd</Text>
                <Text>Another text</Text>
            </RequestData>
            <RequestData ID="s893hwkldja" Date="12-12-2005"/>
        </SampleSoapRequest>
    </SOAP:Body>
</SOAP:Envelop>

使用流行的 Document Object Model (DOM),定义了多种类型的节点,以及获取同一个 DOM 树所需的多种函数,例如 createAttribute(String name)Element createElement(String tagName)。然而,通过调用至少一个方法来创建每一个 attribute/element/textNode 可能会出错,尤其是当 DOM 对象没有暴露最终 XML 文档/元素的外观时,很难监控 DOM 树的变化。

通过 EasyXML,XML 文档被大大简化,只关注 XML ElementXML Attribute。传统上,XML Element 的值始终是 null,并且 Element 的开始和结束标签之间的文本被定义为 TextEasyXML 将文本内容(如果非空)视为其直接父 XML Element 的值,并忽略所有其他节点(EntityNotation 等)。因此,EasyXML 定义了两个类:ElementAttributeAttribute 只是 Element 的一个名称-值对;一个 Element 可以包含多个 Attribute、多个子 Element 和一个 String 类型的值,该值可以是 null(默认情况下,没有 innerText)或一个非空的 String 文本。要定位特定类型的 ElementAttribute,有两种路径:

  • 通过连接 Element 名称并使用 > 来表示 Element 路径

    例如,root>body>message 表示 <message>Element,其父节点是 <root> 下的 <body>Element

  • 通过连接 Element 路径和 Attribute 名称并使用 < 来表示 Attribute 路径

    例如,root>body>message<id 表示 <root><body><message> 的所有 Elementid 属性。

此外,EasyXML 的设计易于调试:您可以清楚地看到 ElementDocument 将以格式良好的方式呈现。也就是说,Element 的显示方式将与 XML 文档中的显示方式相同。

将 XML 解析为 String Maps

虽然从头开始创建 XML 文档可能带来一些便利,但与操作传统的 DOM 对象相比,修改现有 XML 模板的值以生成新 XML 文档仍然可能很繁琐且不太直接。实际上,这也可以通过 EasyXML 实现。然而,要达到这个目标,EasyXML 必须能够有效地解析 XML。

这实际上是我开发此工具包以执行一些 SOAP 服务 API 测试的初衷:通过另一个数据挖掘框架,输入到 SOAP 请求的测试数据和来自响应的结果表示为一个或多个 Map<String, String> 实例,如果 SOAP 响应可以转换为 Map<String, String> 列表,则通过迭代键进行比较将变得流畅且容易得多。

使用当前的 EasyXML,您可以提取特定内容,如下面的示例所示,如何处理 Microsoft Sample XML File (books.xml)

@Test
public void testDOMParser_extractInfo() {
    URL url = Thread.currentThread().getContextClassLoader()
        .getResource("books.xml");

    Document doc = EasySAXParser.parse(url);

    Map<String, String> pathes = new LinkedHashMap<String, String>();   
    pathes.put("book<id", "ID");    
    pathes.put("book>author", "AUTHOR");    
    pathes.put("book>title", "TITLE");  
    pathes.put("book>genre", "GENRE");  
    pathes.put("book>price", "PRICE");  
    pathes.put("book>publish_date", "DATE");    
    pathes.put("book>description", "DESCRIPTION");  
    pathes.put("book>author", "AUTHOR");

    Map<String, String[]> values = doc.extractValues(pathes);

    System.out.println(String.format("%s: %s", "ID",
        "[" + StringUtils.join(values.get("ID"), ", ") + "]"));

    System.out.println(String.format("%s: %s", "PRICE",
        "[" + StringUtils.join(values.get("PRICE"), ", ") + "]"));
}

输出将是(请注意,有 12 个 ID,但只有 11 个价格):

ID: [bk101, bk102, bk103, bk104, bk105, bk106, bk107, bk108, bk109, bk110, bk111, bk112]
PRICE: [5.95, 5.95, 5.95, 5.95, 4.95, 4.95, 4.95, 6.95, 36.95, 36.95, 49.95]

然而,String 矩阵很难转换为对象,例如具有 IDPRICE 属性的书籍。

将每个对象转换为一个独立的 map 会更有意义,而且非常简单:解析 XML 文档只需调用一个函数,该函数只有一个 String 参数来指定目标 Element。对于前面的 books.xml,您可以指定要解析的 Element 为“book”。

@Test
public void testDocument_mapOf() {

    URL url = Thread.currentThread().getContextClassLoader()
        .getResource("books.xml");

    Document doc = EasySAXParser.parse(url);

    List<? extends Map<String, String>> maps = doc.mapOf("book");

    System.out.println(maps.get(0));

    System.out.println(maps.get(1));
}

结果是 12 本书的 12 个 Map<String, String>,对应下面显示的第一个和第二个 XML <book> 元素。

   <book id="bk101">
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
<!--       <price>44.95</price> -->
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications 
      with XML.</description>
   </book>
   <book id="bk102">
      <author>Ralls, Kim</author>
      <title>Midnight Rain</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <publish_date>2000-12-16</publish_date>
      <description>A former architect battles corporate zombies, 
      an evil sorceress, and her own childhood to become queen 
      of the world.</description>
   </book>

打印出对应的第一个和第二个 Map<String, String>

{author=Gambardella, Matthew, genre=Computer, description=An in-depth look at creating applications 
      with XML., id=bk101, title=XML Developer's Guide, publish_date=2000-10-01}
{author=Ralls, Kim, price=5.95, genre=Fantasy, description=A former architect battles corporate zombies, 
      an evil sorceress, and her own childhood to become queen of the world., 
      id=bk102, title=Midnight Rain, publish_date=2000-12-16}

简单直接,无需担心某些 <book> 会比其他 <book> 拥有更多或更少的属性。

此外,假设您已经定义了如何将 Element/Attribute 映射到某些标准键,那么您可以使用一个 Map 来调用该方法,该 Map 包含相关的条目以及如何生成结果键,如下所示:

@Test
public void testDocument_mapOf_WithAliasSpecified() {

    URL url = Thread.currentThread().getContextClassLoader()
        .getResource("books.xml");

    Document doc = EasySAXParser.parse(url);

    Map<String, String> pathes = new LinkedHashMap<String, String>();

    pathes.put("id", "ID");

    pathes.put("author", "AUTHOR");

    pathes.put("title", "TITLE");

    pathes.put("genre", "GENRE");

    pathes.put("price", "PRICE");

    pathes.put("publish_date", "DATE");

    //pathes.put("description", "DESCRIPTION");

    pathes.put("author", "AUTHOR");

    List<? extends Map<String, String>> maps = doc.mapOf("book", pathes);

    System.out.println(maps.get(0));

    System.out.println(maps.get(1));
}

那么前两本书将表示为以下两个 map:

{DATE=2000-10-01, TITLE=XML Developer's Guide, GENRE=Computer, ID=bk101, AUTHOR=Gambardella, Matthew}
{DATE=2000-12-16, PRICE=5.95, TITLE=Midnight Rain, GENRE=Fantasy, ID=bk102, AUTHOR=Ralls, Kim}

请注意,“book”的第一个参数可以替换为任何类型的 Element,然后它们的 attributes/childElements 将自动用于组合 Map<>

例如,在第 1 部分中生成的 SOAP 文档:

@Test
public void testDocument_mapOfDeepElement() throws SAXException {

    if (soapDoc == null) {

        testDocument_composeSOAP();

    }

    List<? extends Map<String, String>> maps = soapDoc
        .mapOf("Credential>User");

    System.out.println(maps);
}

Credential>User 的路径将由 <SampleSoapRequest> 匹配 - DocumentdefaultContainer,然后它与 <User> 元素匹配以获取内容的映射。

[{ID=test01, Password=password01}, {ID=test02}, {ID=test03, Password=password03}]

XML 来自模板

如前所述,使用 XML 会更容易,特别是当已经有一些可用模板时,而不需要从头开始组合 XML 文档。

EasyXML 使用传统的 DOM 或 SAX 解析器将 XML 转换为自己的 Document 对象。首选 XML 源文件的 URL:您可以将 .DTD 文件与 .XML 文件放在一起,DOM 和 SAX 解析器都可以通过调用 DomParserEasySAXParserstatic 函数来自动验证模式。

Document doc = EasySAXParser.parse(url);

或者

Document doc = DomParser.parse(url);

然后,您可以使用解析后的 DocumentElements 来方便地构建新的 Documents,如下面的示例所示:

@Test
public void testDocument_createNewBasedOnExisting() {

    URL url = Thread.currentThread().getContextClassLoader()
        .getResource("books.xml");

    Document doc = EasySAXParser.parse(url);
    List<Element> books = doc.getElementsOf("book");

    Document newDoc = new Document("books");
    for (int i = 0; i < 4; i ++) {
        newDoc.addChildElement(books.get(i));
    }

    newDoc.setValuesOf("book<id", "101", "102", "103", "104");
    newDoc.setValuesOf("book>description", "", "", "", "");

    String outcomeXml = newDoc.toString(0, false, false);
    System.out.println(outcomeXml);

//  Document brandnewDoc = EasySAXParser.parseText(outcomeXml);
//  System.out.print(brandnewDoc.toString());
}

newDoc 实例由解析后的 Document doc 的前四个 <book> Elements 组成,然后可以批量更改它们的 id 并清除 description,以获得以下输出:

<books>
    <book id="101">
        <author>Gambardella, Matthew</author>
        <title>XML Developer&apos;s Guide</title>
        <genre>Computer</genre>
        <publish_date>2000-10-01</publish_date>
    </book>
    <book id="102">
        <author>Ralls, Kim</author>
        <title>Midnight Rain</title>
        <genre>Fantasy</genre>
        <price>5.95</price>
        <publish_date>2000-12-16</publish_date>
    </book>
    <book id="103">
        <author>Corets, Eva</author>
        <title>Maeve Ascendant</title>
        <genre>Fantasy</genre>
        <price>5.95</price>
        <publish_date>2000-11-17</publish_date>
    </book>
    <book id="104">
        <author>Corets, Eva</author>
        <title>Oberon&apos;s Legacy</title>
        <genre>Fantasy</genre>
        <price>5.95</price>
        <publish_date>2001-03-10</publish_date>
    </book>
</books>

请注意,对于 newDoc Document,其 <book> Elements 仍然包含一些空的 <description> 节点,因为 EasyXML 为了简单起见,尚未实现删除/插入/重命名 Attributes 或 Child Elements。但是,newDoc.toString(0, false, false) 可以防止它们被显示。

出于同样的原因,原始 doc Document 仍然包含 12 个 <book> Elements,前四个将与 newDoc Document 共享。

通过这种方式,可以完全“借用”和“定制”从多个 .XML 文件解析的多个 Document 中“借用”和“定制” Elements 来组合一个极其复杂的 Document,如果您真的担心隐藏的 Element,例如那些 <descripttion> 节点,那么 commentedEasySAXParser.parseText(outcomeXml) 将毫不费力地提供一个独立的 Document

将 XML 转换为 JSON

EasyXML 也可以用作将 XML Element/Document 转换为 JSON strings 的工具。

JsonTest.java 提供了足够的示例。以 glossary.xml 为例,以下代码:

@Test
public void testDocument_toJSON4() {

    URL url = Thread.currentThread().getContextClassLoader()
        .getResource("glossary.xml");

    Document doc = EasySAXParser.parse(url);

    String json = doc.toJSON();

    System.out.println(json);
}

将产生此输出:

"glossary":{
    "title":"example glossary",
    "GlossDiv":{
        "title":"S",
        "GlossList":{
            "GlossEntry":{
                "ID": "SGML",
                "SortAs": "SGML",
                "GlossTerm":"Standard Generalized Markup Language",
                "Acronym":"SGML",
                "Abbrev":"ISO 8879:1986",
                "GlossDef":{
                    "para":"A meta-markup language, used to create markup
                        languages such as DocBook.",
                    "GlossSeeAlso":[
                        {
                            "OtherTerm": "GML"
                        },
                        {
                            "OtherTerm": "XML"
                        }
                    ]
                },
                "GlossSee":{
                    "OtherTerm": "markup"
                }
            }
        }
    }
}

还可以通过预定义的键将某些特定的 Elements 转换为 JSON,如下所示:

@Test
public void testDocument_toJSON() {
    URL url = Thread.currentThread().getContextClassLoader()
        .getResource("books.xml");

    Document doc = EasySAXParser.parse(url);

    Map<String, String> pathes = new LinkedHashMap<String, String>();   
    pathes.put("ID", "book<id");    
    pathes.put("AUTHOR", "book>author");    
    pathes.put("TITLE", "book>title");  
    pathes.put("GENRE", "book>genre");

    String json = doc.toJSON("book", pathes);   
    System.out.println(json);
}

输出

    {book{"ID":"bk101", 
    "AUTHOR":"Gambardella, Matthew", 
    "TITLE":"XML Developer's Guide", 
    "GENRE":"Computer"},
    book{"ID":"bk102", 
    "AUTHOR":"Ralls, Kim", 
    "TITLE":"Midnight Rain", 
    "GENRE":"Fantasy"},
    book{"ID":"bk103", 
    "AUTHOR":"Corets, Eva", 
    "TITLE":"Maeve Ascendant", 
    "GENRE":"Fantasy"},
    book{"ID":"bk104", 
    "AUTHOR":"Corets, Eva", 
    "TITLE":"Oberon's Legacy", 
    "GENRE":"Fantasy"},
    book{"ID":"bk105", 
    "AUTHOR":"Corets, Eva", 
    "TITLE":"The Sundered Grail", 
    "GENRE":"Fantasy"},
    book{"ID":"bk106", 
    "AUTHOR":"Randall, Cynthia", 
    "TITLE":"Lover Birds", 
    "GENRE":"Romance"},
    book{"ID":"bk107", 
    "AUTHOR":"Thurman, Paula", 
    "TITLE":"Splish Splash", 
    "GENRE":"Romance"},
    book{"ID":"bk108", 
    "AUTHOR":"Knorr, Stefan", 
    "TITLE":"Creepy Crawlies", 
    "GENRE":"Horror"},
    book{"ID":"bk109", 
    "AUTHOR":"Kress, Peter", 
    "TITLE":"Paradox Lost", 
    "GENRE":"Science Fiction"},
    book{"ID":"bk110", 
    "AUTHOR":"O'Brien, Tim", 
    "TITLE":"Microsoft .NET: The Programming Bible", 
    "GENRE":"Computer"},
    book{"ID":"bk111", 
    "AUTHOR":"O'Brien, Tim", 
    "TITLE":"MSXML3: A Comprehensive Guide", 
    "GENRE":"Computer"},
    book{"ID":"bk112", 
    "AUTHOR":"Galos, Mike", 
    "TITLE":"Visual Studio 7: A Comprehensive Guide", 
    "GENRE":"Computer"}}

这与以下代码的输出截然不同:

@Test
public void testDocument_toJSON3() {

    URL url = Thread.currentThread().getContextClassLoader()
        .getResource("books.xml");

    Document doc = EasySAXParser.parse(url);

    String json = doc.toJSON();

    System.out.println(json);
}

即:

"catalog":{
    "book":[
        {
            "id": "bk101",
            "author":"Gambardella, Matthew",
            "title":"XML Developer's Guide",
            "genre":"Computer",
            "publish_date":"2000-10-01",
            "description":"An in-depth look at creating applications 
      with XML."
        },
        ...Omitted here........
        {
            "id": "bk112",
            "author":"Galos, Mike",
            "title":"Visual Studio 7: A Comprehensive Guide",
            "genre":"Computer",
            "price":"49.95",
            "publish_date":"2001-04-16",
            "description":"Microsoft Visual Studio 7 is explored in depth,
                           looking at how Visual Basic, Visual C++, C#, 
                           and ASP+ are integrated into a comprehensive development environment."
        }
    ]
}

至此,EasyXML 的简要应用介绍完毕。

Using the Code

要了解如何使用它,请运行 'src/test/java' 下的测试,包括上面描述的测试。

历史

  • 2015年5月22日:首个版本发布
© . All rights reserved.