SAX,功能強大的 API


https://www.ibm.com/developerworks/cn/xml/x-saxapi/

SAX,功能強大的 API

在摘自 XML by Example 的本篇預覽中比較了 DOM 和 SAX,然后開始使用 SAX

這篇對 Benoit Marchal 所著的 XML by Example第二版的預覽給出了對 SAX 的翔實介紹,SAX 是用於處理 XML 的基於事件的 API,它已經成為事實上的標准。本篇預覽講述了何時使用 SAX 替換 DOM,概述了常用的 SAX 接口,並在基於 Java 的應用程序中提供了帶有許多代碼樣本的詳細示例。 

本文由即將出版的 XML by Example第二版中的一章改編,介紹了 SAX,它是用於處理 XML 的基於事件的 API,SAX 是對“文檔對象模型”或者 DOM 的補充,DOM 是用於由 W3C 發布的 XML 語法分析器的基於對象的 API。

您將了解到,SAX:

  • 是基於事件的 API。
  • 在一個比 DOM 低的級別上操作。
  • 為您提供比 DOM 更多的控制。
  • 幾乎總是比 DOM 更有效率。
  • 但不幸的是,需要比 DOM 更多的工作。

為什么出現另一個 API?

不要被名稱欺騙。SAX 可能是 Simple API for XML,但它需要比 DOM 更多的工作。其回報 - 更緊湊的代碼 - 是值得努力的。

圖 1 顯示了典型 XML 程序的兩個組件:

  • 語法分析器,代表應用程序解碼 XML 文件的軟件組件。語法分析器有效地使開發者避開復雜的 XML 語法。
  • 應用程序,它使用文件內容。
圖 1. XML 程序的體系結構
XML 應用程序體系結構的流程圖

顯然,應用程序可以很簡單(例如,在歐元和美元之間轉換價格的應用程序)也可以非常復雜,例如,通過因特網訂購貨物的分布式電子貿易應用程序。

本章集中討論圖 1 中的虛線 - 語法分析器和應用程序之間的接口或 API(應用程序編程接口)。

 

基於對象和基於事件的接口

您可能已經知道語法分析器有兩類接口 - 基於對象的和基於事件的接口。

在拙作的另一章中詳細討論了由 W3C 開發並發布的 DOM,它是基於對象的語法分析器的標准 API。這個關於 DOM 的簡要概述只為您提供背景知識,以便您更好地全面理解 SAX。

作為基於對象的接口,DOM 通過在內存中顯示地構建對象樹來與應用程序通信。對象樹是 XML 文件中元素樹的精確映射。

DOM 易於學習和使用,因為它與基本 XML 文檔緊密匹配。它對於我稱為以 XML 為中心的應用程序(例如,瀏覽器和編輯器)也是很理想的。以 XML 為中心的應用程序為了操縱 XML 文檔而操縱 XML 文檔。

然而,對於大多數應用程序,處理 XML 文檔只是其眾多任務中的一種。例如,記帳軟件包可能導入 XML 發票,但這不是其主要活動。計算帳戶余額、跟蹤支出以及使付款與發票匹配才是主要活動。記帳軟件包可能已經具有一個數據結構(最有可能是數據庫)。DOM 模型不太適合記帳應用程序,因為在那種情況下,應用程序必須在內存中維護數據的兩份副本(一個是 DOM 樹,另一個是應用程序自己的結構)。至少,在內存維護兩次數據會使效率下降。對於桌面應用程序來說,這可能不是主要問題,但是它可能導致服務器癱瘓。

對於不以 XML 為中心的應用程序,SAX 是明智的選擇。實際上,SAX 並不在內存中顯式地構建文檔樹。它使應用程序能用最有效率的方法存儲數據。

圖 2 說明了應用程序如何在 XML 樹及其自身數據結構之間進行映射。

圖 2. 將 XML 結構映射成應用程序結構
圖說明 XML 結構如何映射成應用程序結構

基於事件的接口

正如其名稱所暗示的,基於事件的語法分析器將事件發送給應用程序。這些事件類似於用戶界面事件,例如,瀏覽器中的 ONCLICK 事件或者 Java 中的 AWT/Swing 事件。

事件通知應用程序發生了某件事並需要應用程序作出反應。在瀏覽器中,通常為響應用戶操作而生成事件:當用戶單擊按鈕時,按鈕產生一個ONCLICK 事件。

在 XML 語法分析器中,事件與用戶操作無關,而與正在讀取的 XML 文檔中的元素有關。有對於以下方面的事件:

  • 元素開始和結束標記
  • 元素內容
  • 實體
  • 語法分析錯誤

圖 3 顯示語法分析器在讀取文檔時如何生成事件。

圖 3. 語法分析器生成事件
圖顯示語法分析器如何生成事件

清單 1 顯示了 XML 格式的清單。它詳細列出了不同公司對 XML 培訓的收費。圖 4 顯示了價目表文檔的結構。

清單 1. pricelist.xml
<?xml version="1.0"?>
<xbe:price-list xmlns:xbe="http://www.psol.com/xbe2/listing8.1">
   <xbe:product>XML Training</xbe:product>
   <xbe:price-quote price="999.00"  vendor="Playfield Training"/>
   <xbe:price-quote price="699.00"  vendor="XMLi"/>
   <xbe:price-quote price="799.00"  vendor="WriteIT"/>
   <xbe:price-quote price="1999.00" vendor="Emailaholic"/>
</xbe:price-list>
圖 4. 價目表的結構
價目表的結構圖

XML 語法分析器讀取並解釋該文檔。每當它識別出文檔中的某些內容,就會生成一個事件。

讀取 清單 1 時,語法分析器首先讀取 XML 聲明並生成文檔開始事件。當它遇到第一個開始標記 <xbe:price-list> 時,語法分析器生成它的第二個事件來通知應用程序已經遇到了 price-list 元素。

接下來,語法分析器看到 product 元素的開始標記(為簡單起見,在本文其余部分,我將忽略名稱空格和縮進空格)並生成它的第三個事件。

在開始標記后,語法分析器看到 product 元素的內容: XML Training ,它產生另一個事件。

下一個事件指出 product 元素的結束標記。語法分析器已經完成了對 product 元素的語法分析。到目前為止,它已經激發了 5 個事件:product 元素的 3 個事件,一個文檔開始事件和一個 price-list 開始標記事件。

語法分析器現在移動到第一個 price-quote 元素。它為每個 price-quote 元素生成兩個事件:一個開始標記事件和一個結束標記事件。

是的,即使將結束標記簡化為開始標記中的 / 字符,語法分析器仍然生成一個結束事件。

有 4 個 price-quote 元素,所以語法分析器在分析它們時生成 8 個事件。最后,語法分析器遇到 price-list 的結束標記並生成它的最后兩個事件:結束 price-list 和文檔結束。

如圖 5 所示,這些事件共同向應用程序描述了文檔樹。開始標記事件意味着“轉到樹的下一層”,而結束標記元素意味着“轉到樹的上一層”。

圖 5. 語法分析器如何隱含地構建樹
圖說明 SAX 語法分析器如何隱含地構建 DOM 樹

請注意,語法分析器傳遞了足夠信息以構建 XML 文檔的文檔樹,但是與 DOM 語法分析器不同,它並不顯式地構建該樹。

為什么使用基於事件的接口?

現在,我敢肯定你已經糊塗了。應該使用哪一種類型的 API,應該何時使用它 - SAX 還是 DOM?不幸的是,這個問題沒有明確的答案。這兩種 API 中沒有一種在本質上更好;他們適用於不同的需求。

經驗法則是在需要更多控制時使用 SAX;要增加方便性時,則使用 DOM。例如,DOM 在腳本語言中很流行。

注:自然的接口

對於語法分析器來說,基於事件的接口是最理想的選擇:它只需報告它看見了什么。

采用 SAX 的主要原因是效率。SAX 比 DOM 做的事要少,但提供了對語法分析器的更多控制。當然,如果語法分析器的工作減少,則意味着您(開發者)有更多的工作要做。

而且,正如我們已討論的,SAX 比 DOM 消耗的資源要少,這只是因為它不需要構建文檔樹。

在 XML 早期,DOM 得益於 W3C 批准的官方 API 這一身份。逐漸地,開發者選擇了功能性而放棄了方便性,並轉向了 SAX。

SAX 的主要限制是它無法向后瀏覽文檔。實際上,激發一個事件后,語法分析器就將其忘記。如您將看到的,應用程序必須顯式地緩沖其感興趣的事件。

注:SAX 構建的樹

如果需要,應用程序可以用它從語法分析器接收的事件構建 DOM 樹。事實上,幾個 DOM 語法分析器是在 SAX 語法分析器的基礎上構建的。

 

SAX,功能強大的 API

當然,無論它實現 SAX 還是 DOM API,語法分析器都做許多工作:它讀取文檔,強制實施 XML 語法並解析實體 - 先只列舉這幾個。驗證語法分析器還強制實施文檔模式。

使用語法分析器有很多原因,並且您應該掌握 API、SAX 和 DOM。它使您能靈活地根據手上的任務來選擇最好的 API。幸好,現代語法分析器同時支持兩種 API。

SAX 是由 XML-DEV 郵件列表的成員開發的一種用於基於事件的語法分析器的標准和簡單的 API。SAX 是“Simple API for XML”的縮寫。

SAX 最初是為 Java 而定義,但是它也可以用於 Python、Perl、C++ 和 COM(Windows 對象)。以后一定還有更多的語言綁定。而且,通過 COM,SAX 語法分析器還可以用於所有 Windows 編程語言,包括 Visual Basic 和 Delphi。

與 DOM 不同,SAX 沒有經過官方標准機構的認可,但是它被廣泛使用並被視為事實上的標准。(現在,SAX 由 David Megginson 編輯,但是他已經宣布將要退休。)

如您所見,在瀏覽器中,DOM 是首選的 API。因此,本章中的示例是用 Java 編寫的。(如果您覺得需要一個 Java 速成課程,請轉至拙作的附錄 A 或者 developerWorks Java 區的“教學”部分。)

一些支持 SAX 的語法分析器包括 Xerces,Apache parser(以前的 IBM 語法分析器)、MSXML(Microsoft 語法分析器)和 XDK(Oracle 語法分析器)。這些語法分析器是最靈活的,因為它們還支持 DOM。

有幾個語法分析器僅提供 SAX,例如 James Clark 的 XP和 Vivid Creations 的 ActiveSAX(請參閱 參考資料)。

 

SAX 入門

清單 2是查找清單 1 中最便宜價格的 Java 應用程序。該應用程序打印出最優的價格和供應商名稱。

編譯示例

要編這個應用程序,需要適用於您平台的“Java 開發工具箱(JDK)”(請參閱 參考資料)。對於該示例,Java Runtime(Java 運行時環境)是不夠的。

注意

Java 難以處理包含空格的路徑。如果“最便宜”的公司抱怨它無法找到文件,請檢查目錄中的錯誤空格。

從 作者網站的 XBE2 頁面下載本摘錄的清單。下載內容包括 Xerces。如果清單有問題,請訪問作者網站以獲取更新。

在名為 Cheapest.java 的文件中保存 清單 2 。轉至 DOS 提示符,更改到保存Cheapest.java 的目錄,然后在 DOS 提示符處發出下列命令來編譯:

mkdir classes
set classpath=classes;lib\xerces.jar  
javac -d classes src\Cheapest.java

編譯將在 classes 目錄中安裝 Java 程序。這些命令假設您已經在 lib 目錄中安裝了 Xerces,並且在 src 目錄中安裝了清單 2。如果在另一個目錄下安裝語法分析器,則可能必須修改 classpath (第二條命令)。

要對價目表運行應用程序,請發出下面的命令:

java com.psol.xbe2.Cheapest data\pricelist.xml

結果應該是:

The cheapest offer is from XMLi ($699.00)

這條命令假設 清單 1在一個名為 data\pricelist.xml 的文件中。同樣,您可能需要修改系統路徑。

技巧:關於事件處理器

事件處理器不調用語法分析器。實際上正好相反:語法分析器調用事件處理器。困惑了?想想 AWT 事件。連接到按鈕的事件處理器不調用按鈕。它等待按鈕被單擊。

事件處理器的逐步討論

將 SAX 中的事件定義為連接到特定 Java 接口的方法。本節將逐步復查清單 2。下面一節為您提供關於主要 SAX 接口的更多信息。

聲明事件處理器的最簡單方案是繼承 SAX 提供的 DefaultHandler :

public class Cheapest
   extends DefaultHandler

該應用程序僅實現一個事件處理器 startElement() ,語法分析器在遇到開始標記時調用它。語法分析器將對文檔 <xbe:price-list> 、<xbe:product> 和 <xbe:price-quote> 中的每個開始標記調用 startElement() 。

在清單 2 中,事件處理器僅對 price-quote 感興趣,所以僅對它測試。該處理器對其它元素的事件不作任何處理。

if(uri.equals(NAMESPACE_URI) && name.equals("price-quote"))
{
   // ...
}

當事件處理器發現 price-quote 元素時,它從屬性列表中抽取供應商名稱和價格。有了這些信息,查找最便宜的產品就是一個簡單的比較處理了。

String attribute =
   attributes.getValue("","price");
if(null != attribute)
{
   double price = toDouble(attribute);
   if(min > price)
   {
      min = price;
      vendor = attributes.getValue("","vendor");
   }
}

請注意,事件處理器接收元素名稱、名稱空間和屬性列表作為來自語法分析器的參數。

現在,讓我們將注意力轉向 main() 方法。它創建一個事件處理器對象和一個語法分析器對象:

Cheapest cheapest = new Cheapest();
XMLReader parser =
   XMLReaderFactory.createXMLReader(PARSER_NAME);

XMLReader 和 XMLReaderFactory 由 SAX 定義。 XMLReader 是一種 SAX 語法分析器。factory 是用於創建 XMLReaders 的幫助器類。

main() 設置一個語法分析器功能以請求名稱空間處理,並且使用語法分析器注冊事件處理器。最后,main() 使用至 XML 文件的 URI 調用 parse() 方法:

parser.setFeature("http://xml.org/sax/features/namespaces",true);
parser.setContentHandler(cheapest);
parser.parse(args[0]);

技巧:名稱空間

缺省情況下,將 http://xml.org/sax/features/namespaces 設置為真,但是顯式地將它設置為真將使代碼更具可讀性。

看似無關的 parse() 方法觸發對 XML 文檔的語法分析,這導致了調用事件處理器。我們的startElement() 方法正是在執行這個方法期間被調用的。在調用 parse() 背后發生了很多事情。

最后但很重要的一點, main() 打印出結果:

Object[] objects = new Object[]
{
   cheapest.vendor,
   new Double(cheapest.min)
};
System.out.println(MessageFormat.format(MESSAGE,objects));

等一下! Cheapest.vendor 和 Cheapest.min 何時獲取它們的值?我們不在 main() 中顯式地設置它們!確實如此;這是事件處理器的工作。最后由 parse() 調用事件處理器。這就是事件處理的美妙之處。

注意
請記住,除非已經安裝了“Java 開發工具箱”,否則不能編譯這些示例。最后,可能有一個錯誤類似於:
 
src\Cheapest.java:7: Package org.xml.sax 
  not found in import.
import org.xml.sax.*;

 
Can't find class com/psol/xbe2/Cheapest 
   or something it requires

這極有可能出自以下原因:
  • 類路徑(第二個命令, classes;lib\xerces.jar) )不正確。
  • 在最后一個命令 (com.psol.xbe2.Cheapest) 中輸入了不正確的類名稱。
 

常用的 SAX 接口和類

到目前為止,我們僅討論了一個事件( startElement() )。在繼續之前,讓我們研究一下 SAX 定義的主接口。

注:SAX 版本

到目前為止,有兩個 SAX 版本:SAX1 和 SAX2。本章僅介紹 SAX2 API。SAX1 與 SAX2 很相似,但是它缺少名稱空間處理。

SAX 將其事件分為幾個接口:

  • ContentHandler 定義與文檔本身關聯的事件(例如,開始和結束標記)。大多數應用程序都注冊這些事件。
  • DTDHandler 定義與 DTD 關聯的事件。然而,它不定義足夠的事件來完整地報告 DTD。如果需要對 DTD 進行語法分析,請使用可選的 DeclHandler。DeclHandler 是 SAX 的擴展,並且不是所有的語法分析器都支持它。
  • EntityResolver 定義與裝入實體關聯的事件。只有少數幾個應用程序注冊這些事件。
  • ErrorHandler 定義錯誤事件。許多應用程序注冊這些事件以便用它們自己的方式報錯。

注:SAX 的成功之處

本節不是 SAX 的全面參考。相反,它集中討論最常用的類。

為簡化工作,SAX 在 DefaultHandler 類中提供了這些接口的缺省實現。在大多數情況下,為應用程序擴展 DefaultHandler 並覆蓋相關的方法要比直接實現一個接口更容易。

XMLReader

為注冊事件處理器並啟動語法分析器,應用程序使用 XMLReader 接口。如我們所見,parse() ,這種 XMLReader 方法,啟動語法分析:

parser.parse(args[0]);

XMLReader 的主要方法是:

  • parse() 對 XML 文檔進行語法分析。 parse() 有兩個版本;一個接受文件名或 URL,另一個接受 InputSource 對象(請參閱“InputSource”一節)。
  • setContentHandler() 、 setDTDHandler() 、 setEntityResolver() 和 setErrorHandler() 讓應用程序注冊事件處理器。
  • setFeature() 和 setProperty() 控制語法分析器如何工作。它們采用一個特性或功能標識(一個類似於名稱空間的 URI 和值)。功能采用 Boolean 值,而特性采用“對象”。

最常用的 XMLReaderFactory 功能是:

  • http:// xml.org/sax/features/namespaces ,所有 SAX 語法分析器都能識別它。如果將它設置為 true(缺省值),則在調用ContentHandler 的方法時,語法分析器將識別出名稱空間並解析前綴。
  • http://xml.org/sax/features/validation,它是可選的。如果將它設置為 true,則驗證語法分析器將驗證該文檔。非驗證語法分析器忽略該功能。

XMLReaderFactory

XMLReaderFactory 創建語法分析器對象。它定義 createXMLReader() 的兩個版本:一個采用語法分析器的類名作為參數,另一個從org.xml.sax.driver 系統特性中獲得類名稱。

對於 Xerces,類是 org.apache.xerces.parsers.SAXParser 。應該使用 XMLReaderFactory ,因為它易於切換至另一種 SAX 語法分析器。實際上,只需要更改一行然后重新編譯。

XMLReader parser = XMLReaderFactory.createXMLReader(
                       "org.apache.xerces.parsers.SAXParser");

為獲得更大的靈活性,應用程序可以從命令行讀取類名或使用不帶參數的 createXMLReader() 。因此,甚至可以不重新編譯就更改語法分析器。

InputSource

InputSource 控制語法分析器如何讀取文件,包括 XML 文檔和實體。

在大多數情況下,文檔是從 URL 裝入的。但是,有特殊需求的應用程序可以覆蓋 InputSource 。例如,這可以用來從數據庫中裝入文檔。

ContentHandler

ContentHandler 是最常用的 SAX 接口,因為它定義 XML 文檔的事件。

如您所見, 清單 2 實現在 ContentHandler 中定義的事件 startElement() 。它用語法分析器注冊 ContentHandler :

Cheapest cheapest = new Cheapest();
// ...
parser.setContentHandler(cheapest);

ContentHandler 聲明下列事件:

  • startDocument() / endDocument() 通知應用程序文檔的開始或結束。
  • startElement() / endElement() 通知應用程序標記的開始或結束。屬性作為 Attributes 參數傳遞(請參閱下面一節“屬性”)。即使只有一個標記,“空”元素(例如, <img href="logo.gif"/> )也生成 startElement() 和 endElement() 。
  • startPrefixMapping() / endPrefixMapping() 通知應用程序名稱空間作用域。您幾乎不需要該信息,因為當http://xml.org/sax/features/namespaces 為 true 時,語法分析器已經解析了名稱空間。
  • 當語法分析器在元素中發現文本(已經過語法分析的字符數據)時, characters() / ignorableWhitespace() 會通知應用程序。要知道,語法分析器負責將文本分配到幾個事件(更好地管理其緩沖區)。 ignorableWhitespace 事件用於由 XML 標准定義的可忽略空格。
  • processingInstruction() 將處理指令通知應用程序。
  • skippedEntity() 通知應用程序已經跳過了一個實體(即,當語法分析器未在 DTD/schema 中發現實體聲明時)。
  • setDocumentLocator() 將 Locator 對象傳遞到應用程序;請參閱后面的 Locator 一節。請注意,不需要 SAX 語法分析器提供 Locator,但是如果它提供了,則必須在任何其它事件之前激活該事件。

屬性
在 startElement() 事件中,應用程序在 Attributes 參數中接收屬性列表。

String attribute = attributes.getValue("","price");

Attributes 定義下列方法:

  • getValue(i) / getValue(qName) /getValue(uri,localName) 返回第 i 個屬性值或給定名稱的屬性值。
  • getLength() 返回屬性數目。
  • getQName(i) / getLocalName(i) /getURI(i) 返回限定名(帶前綴)、本地名(不帶前綴)和第 i 個屬性的名稱空間 URI。
  • getType(i)/getType(qName)/getType(uri,localName) 返回第 i 個屬性的類型或者給定名稱的屬性類型。類型為字符串,即在 DTD 所使用的:“CDATA” 、 “ID” 、 “IDREF” 、 “IDREFS” 、 “NMTOKEN” 、 “NMTOKENS” 、 “ENTITY” 、 “ENTITIES” 或 “NOTATION”

注意

Attributes 參數僅在 startElement() 事件期間可用。如果在事件之間需要它,則用 AttributesImpl 復制一個。

定位器

Locator 為應用程序提供行和列的位置。不需要語法分析器來提供 Locator 對象。

Locator 定義下列方法:

  • getColumnNumber() 返回當前事件結束時所在的那一列。在 endElement() 事件中,它將返回結束標記所在的最后一列。
  • getLineNumber() 返回當前事件結束時所在的行。在 endElement() 事件中,它將返回結束標記所在的行。
  • getPublicId() 返回當前文檔事件的公共標識。
  • getSystemId() 返回當前文檔事件的系統標識。

DTDHandler

DTDHandler 聲明兩個與 DTD 語法分析器相關的事件。

  • notationDecl() 通知應用程序已經聲明了一個標記。
  • nparsedEntityDecl() 通知應用程序已經發現了一個未經過語法分析的實體聲明。

EntityResolver

EntityResolver 接口僅定義一個事件 resolveEntity() ,它返回 InputSource (在另一章討論)。

因為 SAX 語法分析器已經可以解析大多數 URL,所以很少應用程序實現 EntityResolver 。例外情況是目錄文件(在另一章中討論),它將公共標識解析成系統標識。如果在應用程序中需要目錄文件,請下載 Norman Walsh 的目錄軟件包(請參閱 參考資料)。

ErrorHandler

ErrorHandler 接口定義錯誤事件。處理這些事件的應用程序可以提供定制錯誤處理。

安裝了定制錯誤處理器后,語法分析器不再拋出異常。拋出異常是事件處理器的責任。

接口定義了與錯誤的三個級別或嚴重性對應的三個方法:

  • warning() 警示那些不是由 XML 規范定義的錯誤。例如,當沒有 XML 聲明時,某些語法分析器發出警告。它不是錯誤(因為聲明是可選的),但是它可能值得注意。
  • error() 警示那些由 XML 規范定義的錯誤。
  • fatalError() 警示那些由 XML 規范定義的致命錯誤。

SAXException

SAX 定義的大多數方法都可以拋出 SAXException 。當對 XML 文檔進行語法分析時, SAXException 通知一個錯誤。

錯誤可以是語法分析錯誤也可以是事件處理器中的錯誤。要報告來自事件處理器的其它異常,可以將異常封裝在 SAXException 中。

示例: 假設在處理 startElement 事件時,事件處理器捕獲了一個 IndexOutOfBoundsException 。事件處理器可以將IndexOutOfBoundsException 封裝在 SAXException 中:

public void startElement(String uri,
                         String name,
                         String qualifiedName,
                         Attributes attributes)
{
   try
   {
      // the code may throw an IndexOutOfBoundsException
   }
   catch(IndexOutOfBounds e)
   {
      throw new SAXException(e);
   }
}

SAXException 一直向上傳遞到 parse() 方法,它在那里被捕獲並進行解釋。

try
{
   parser.parse(uri);
}
catch(SAXException e)
{
   Exception x = e.getException();
   if(null != x)
      if(x instanceof IndexOutOfBoundsException)
         // process the IndexOutOfBoundsException
}
 

維護狀態

清單 1 對於 SAX 語法分析器是很方便的,因為它將信息存儲為價格元素的屬性。應用程序只需要注冊 startElement() 。

示例清單 3 更復雜,因為信息分散到了幾個元素中。特別是,根據不同的交付延遲,供應商有不同的價格。如果用戶願意等待,他(或她)可能得到更好的價格。圖 6 演示了文檔結構。

清單 3. xtpricelist.xml
<?xml version="1.0"?>
<xbe:price-list xmlns:xbe="http://www.psol.com/xbe2/listing8.3">
   <xbe:name>XML Training</xbe:name>
   <xbe:vendor>
      <xbe:name>Playfield Training</xbe:name>
      <xbe:price-quote delivery="5">999.00</xbe:price-quote>
      <xbe:price-quote delivery="15">899.00</xbe:price-quote>
   </xbe:vendor>
   <xbe:vendor>
      <xbe:name>XMLi</xbe:name>
      <xbe:price-quote delivery="3">2999.00</xbe:price-quote>
      <xbe:price-quote delivery="30">1499.00</xbe:price-quote>
      <xbe:price-quote delivery="45">699.00</xbe:price-quote>
   </xbe:vendor>
   <xbe:vendor>
      <xbe:name>WriteIT</xbe:name>
      <xbe:price-quote delivery="5">799.00</xbe:price-quote>
      <xbe:price-quote delivery="15">899.00</xbe:price-quote>
   </xbe:vendor>
   <xbe:vendor>
      <xbe:name>Emailaholic</xbe:name>
      <xbe:price-quote delivery="1">1999.00</xbe:price-quote>
   </xbe:vendor>
</xbe:price-list>
圖 6. 價目表結構
價目表結構圖

要找到最好的生意,應用程序必須從幾個元素搜集信息。但是,語法分析器可以最多為每個元素生成三個事件 - startElement() 、characters() 和 endElement() 。應用程序必須以某種方法將事件和元素相關聯。

清單 4是一個新建的 Java 應用程序,它查找價目表中的最優價格。當查找最優價格時,它考慮到了客戶對交付日期的需求。實際上,清單 3 中最便宜的供應商(XMLi)也是最慢的。另一方面,Emailaholic 很貴,但是它可以在兩天內交付。

您可以如前面介紹的 Cheapest 應用程序那樣編譯並運行該應用程序。結果取決於對交付日期的需求。您將注意到這個程序采用兩個參數:文件名和客戶願意等待的最長延遲。

java com.psol.xbe2.BestDeal data/xtpricelist.xml 60

返回:

The best deal is proposed by XMLi. A(n) XML Training delivered[ccc]
in 45 days for $699.00

而:

java com.psol.xbe2.BestDeal data/xtpricelist.xml 3

返回:

The best deal is proposed by Emailaholic. A(n) XML Training[ccc]
delivered in 1 days for $1,999.00
 

分層體系結構

清單 4 是目前為止您所見到的最復雜的應用程序。這沒什么不尋常的:SAX 語法分析器的級別很低,所以應用程序必須接管本來由 DOM 才能完成的大量工作。

應用程序是圍繞兩個類組織的: SAX2BestDeal 和 BestDeal 。 SAX2BestDeal 管理 SAX 語法分析器之間的接口。它用一致的方法來管理狀態並將事件分組。

BestDeal 具有執行價格比較的邏輯。它還以結構形式保持為應用程序而不是為 XML 優化的信息。圖 7 演示了該應用程序的體系結構。圖 8 顯示了 UML 類圖。

圖 7. 應用程序的體系結構
Best Deal 應用程序體系結構圖
圖 8. 應用程序的類圖
Best Deal 應用程序的類圖

SAX2BestDeal 處理幾個事件: startElement() 、 endElement() 和 characters() 。 SAX2BestDeal 一直跟蹤其在文檔樹中的位置。

例如,在 characters() 事件中, SAX2BestDeal 需要知道文本是名稱、價格還是可以忽略的空格。而且,有兩個 name 元素: price-list 的 name 和 vendor 的 name 。

狀態

與 DOM 語法分析器不同,SAX 語法分析器不提供狀態信息。應用程序負責跟蹤它自己的狀態。這有幾個可選實體。清單 4 標識有意義的狀態以及它們之間的轉換。從圖 6 中的文檔結構中獲得該信息並不困難。

很明顯,應用程序將首先遇到 price-list 標記。因此,第一個狀態應該是 位於 price-list 內。 從那里開始,應用程序到達一個 name 。因此,第二個狀態是 位於 price-list 的 name 內。

下一個元素必須是 vendor ,因此第三個狀態是 位於 price-list 的 vendor 內。 第四個狀態是 位於 price-list 的 vendor 的 name內, 因為 name 跟在 vendor 后。

name 后面是一個 price-quote 元素,相應的狀態是 位於 price-list 的 vendor 的 price 內。 隨后,語法分析器遇到已經有狀態存在的price-quote 或 vendor 。

在帶有狀態和轉換的圖上(例如圖 9 所示)會更容易使這個概念可視化。請注意根據您在處理 price-list/name 還是 price-list/vendor/name ,有兩個不同的狀態與兩個不同的名稱元素相關聯。

圖 9. 狀態轉換圖
狀態轉換圖

在清單 4 中狀態變量存儲當前狀態:

final protected int START = 0,
                    PRICE_LIST = 1,
                    PRICE_LIST_NAME = 2,
                    VENDOR = 3,
                    VENDOR_NAME = 4,
                    VENDOR_PRICE_QUOTE = 5;
protected int state = START;

轉換

轉換 1狀態變量的值根據事件而相應更改。在本示例中,elementStart() 更新狀態:

ifswitch(state)
{
   case START:
      if(name.equals("price-list"))
         state = PRICE_LIST;
      break;
   case PRICE_LIST:
      if(name.equals("name"))
         state = PRICE_LIST_NAME;
         // ...
      if(name.equals("vendor"))
         state = VENDOR;
         break;
   case VENDOR:
      if(name.equals("name"))
         state = VENDOR_NAME;
         // ...
      if(name.equals("price-quote"))
         state = VENDOR_PRICE_QUOTE;
         // ...
      break;
}

SAX2BestDeal 有幾個實例變量來存儲當前 name 和 price-quote 的內容。實際上,它維護樹的一個小子集。請注意,與 DOM 不同,它從不擁有整個樹,因為當應用程序使用過 name 和 price-quote 之后,它會廢棄它們。

這是很有效的內存策略。事實上,您可以處理幾十億字節的文件,因為在任何時候,內存中只有一個小子集。

轉換 2 語法分析器對文檔中的每個字符(包括縮進)調用 characters() 。只有記入 name 和 price-quote 中的文本才有意義,因此事件處理器使用狀態。

switch(state)
{
   case PRICE_LIST_NAME:
   case VENDOR_NAME:
   case VENDOR_PRICE_QUOTE:
      buffer.append(chars,start,length);
      break;
}

轉換 3endElement() 的事件處理器更新狀態,並調用 BestDeal 來處理當前元素:switch(state)

{
   case PRICE_LIST_NAME:
      if(name.equals("name"))
      {
         state = PRICE_LIST;
         setProductName(buffer.toString());
         // ...
      }
      break;
   case VENDOR_NAME:
      if(name.equals("name"))
         state = VENDOR;
         // ...
      break;
   case VENDOR_PRICE_QUOTE:
      if(name.equals("price-quote"))
      {
         state = VENDOR;
         // ...
         compare(vendorName,price,delivery);
         // ...
      }
      break;
   case VENDOR:
      if(name.equals("vendor"))
         state = PRICE_LIST;
         // ...
      break;
   case PRICE_LIST:
      if(name.equals("price-list"))
         state = START;
      break;
}

學過的課程

清單 4 是典型的 SAX 應用程序。有一個 SAX 事件處理器( SAX2BestDeal ),它用最適合應用程序的格式將事件打包。

注:狀態還是堆棧?

使用狀態變量的替換方法是使用“ 堆棧 ”。將元素名(或另一個標識)推入 startElement() ;然后在endElement() 中彈出它。

應用程序邏輯(在 BestDeal 中)與事件處理器保持分離。事實上,在很多情況下,都獨立於 XML 來編寫應用程序邏輯。

分層方法在應用程序邏輯和語法分析之間建立一個明顯的分界。

示例也清晰地說明了 SAX 比 DOM 更高效,但是它需要程序員完成更多工作。特別是,程序員必須顯式地管理狀態和狀態之間的轉換。(在 DOM 中,狀態在樹的遞歸遍歷過程中是隱含的。)

 

靈活性

XML 是非常靈活的標准。但實際上,XML 應用程序的靈活性取決於您,程序員,如何創建它們。本節提供一些技巧,以確保您的應用程序利用 XML 的靈活性。

為靈活性而構建

BestDeal 應用程序對 XML 文檔結構的約束很少。如果在 XML 文檔中添加元素,它們就會被忽略。例如,BestDeal 將接受下列 vendor 元素:

<xbe:vendor>
   <xbe:name>Playfield Training</xbe:name>
   <xbe:contact>John Doe</xbe:contact>
   <xbe:price-quote delivery="5">999.00</xbe:price-quote>
   <xbe:price-quote delivery="15">899.00</xbe:price-quote>
</xbe:vendor>

但是將忽略聯系信息。通常,簡單地忽略未知元素是個好主意 - HTML 瀏覽器就總這樣做。

強制實施結構

但是,從事件處理器驗證它們的結構並不困難。下列代碼片斷(摘自 startElement() )檢查結構,並且如果 vendor 元素包含除名稱或價格以外的任何元素,則拋出 SAXException 。

case VENDOR:
   if(name.equals("name"))
   {
      state = VENDOR_NAME;
      buffer = new StringBuffer();
   }
   else if(name.equals("price-quote"))
   {
      state = VENDOR_PRICE_QUOTE;
      String st = attributes.getValue("","delivery");
      delivery = Integer.parseInt(st);
      buffer = new StringBuffer();
   }
   else
      throw new SAXException("Expecting <xbe:name> or <xbe:price-quote>");
   break;

如果清單帶有 contact 元素,它將報告:

org.xml.sax.SAXException: Expecting <xbe:name> or <xbe:price-quote>

但是,如果實際上應用程序真正依賴於文檔的結構,那么最好編寫一個模式並使用驗證語法分析器。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM