隨筆 - 3, 文章 - 152, 評論 - 17, 引用 - 0
          數據加載中……

          用于XML的簡單API

          Eric Armstrong

          本章主要討論Simple API for XML (SAX),它是一種事件驅動、串行存取XML文檔的機制。這是多數servlet和面向網絡的程序用來傳送和接收XML文檔的協議,因為在目前XML文檔處理的可用機制中它最快并且所需內存最少。

          SAX協議比文檔對象模型(DOM)需要進行更多的編程。它是事件驅動模型(你提供回調方法,解析器在讀取XML數據的時候調用它們),因此很難實現可視化。最后,不能倒退回文檔的前面部分或重新組織它,只能回退到一個串行數據流或重新組織從流中讀取的字符。

          由于上述原因,如果開發者編寫面向用戶的應用程序,且該應用程序顯示XML文檔并可能要對它進行修改,那么就應該使用教程下一部分介紹的DOM機制,文檔對象模型。

          然而,即便是專門建立DOM應用程序,仍然要熟悉SAX模型,主要原因如下:

          • 相同的錯誤處理

          在解析DOM文檔時,會生成相同類型的異常,所以JAXP SAX和DOM應用程序的錯誤處理程序是相同的。

          • 處理驗證錯誤

          默認情況下, 規范要求忽略驗證錯誤(在該教程的這一部分中會詳細介紹) 。如果想要在驗證錯誤事件中拋出一個異常(并且能這樣做),那么就需要理解SAX錯誤處理的工作原理。

          • 轉換現有數據

          可以看到在本教程的DOM節,可以使用一種機制將現有數據集轉換成XML——然而,要充分利用該機制首先必須理解SAX模型。


          注意:可以在下面的地址中找到本章的例子: <JWSDP_HOME>/docs/tutorial/examples/jaxp/sax/samples.

          何時使用 SAX

          當要快速、高效地讀取XML數據時,SAX是無可挑剔的。它對內存的要求很小,因為它不構建XML數據的內部表示(樹結構)。它僅僅將讀取的數據發送給應用程序——然后應用程序就可以隨意處理它所見到的數據。

          實際上,SAX API類似于串行I/O流。在流進入的時候,可以看到數據,但是不能回到以前的位置或跳躍到不同的位置。總的說來,如果僅想讀取數據并在這基礎上運行應用程序它就非常有效。

          理解SAX事件模型對于將現有數據轉換成XML非常有用。在從任意數據結構生成XML 可以看到,轉換過程的關鍵是修改現有應用程序,以便在讀取數據時發送合適的SAX事件。

          但是,當需要修改XML結構時——特別是需要互相修改的時候,使用類似于文檔對象模型(DOM)的內存結構更為合理。

          然而,雖然DOM提供了許多處理大型文檔(如書和論文)的強大功能,但是還是需要進行復雜的編程 (在何時使用DOM中詳細介紹了該處理的細節) 。

          在簡單的應用程序中,這么復雜是完全沒有必要的。在快速開發和簡單的應用程序中,使用面向對象的XML編程標準更加合理,在JDOM 和dom4j中會具體介紹。

          編寫簡單的XML文件

          現在開始編寫一個用來進行幻燈片播放的XML數據的簡單版本。在該練習中,使用文本編輯器創建數據,這樣就能比較適應XML文件的基本格式。你將使用該文件并在以后的練習中對它進行擴展。

          創建文件

          使用標準的文本編輯器,創建一個叫做slideSample.xml的文件。


          注意: 這里已經有一個slideSample01.xml的版本。(可以瀏覽的版本是 slideSample01-xml.html)可以將該版本和你所編寫的相比較,或者僅在閱讀此指南時查看。


          編寫聲明

          下一步,編寫聲明,其中該聲明將文件標識為一個XML文檔。聲明以字符"<?"開始這是處理指令的標準XML標識符。(在本教程的后面部分可以看到其他處理指令)

            <?xml version='1.0' encoding='utf-8'?>  

          該行表明此文檔是一個XML文檔,它遵守XML1.0版本規范,并且使用8-位Unicode字符編碼方案。(要想獲得編碼方案的信息,請查看Java編碼方案 .)

          由于沒有指定文檔是“獨立的”,解析器假設該文檔可能包含到其他文檔的引用。想知道如何將一個文檔指定為“獨立的” 。

          添加注釋

          注釋會被XML解析器忽略。實際上,你根本就看不到它們,除非你激活解析器里的特殊設置。教程后面部分的處理詞法事件中會詳細介紹如何使用它。現在,添加下面突出顯示的內容,在文件中增加一條注釋。

          <?xml version='1.0' encoding='utf-8'?> 
           
          <!-- A SAMPLE set of slides -->  

          定義根(Root)元素

          在聲明之后,每個XML文件都會精確地定義一個元素,這就是根元素。文件中的任何其他元素都包含在該元素中。輸入下面突出顯示文本,定義該文件的根元素slideshow

          <?xml version='1.0' encoding='utf-8'?> 
           
          <!-- A SAMPLE set of slides --> 
           
          <slideshow> 
           
          </slideshow> 

          注意:XML元素命名是大小寫敏感的。結束標簽必須和起始標簽匹配。


          給元素添加屬性

          幻燈片播放有大量相關數據項,它們都不需要任何結構。所以可以將它們定義成slideshow 元素的屬性。添加下面突出顯示的文本,建立一些屬性:

          ...
            <slideshow 
              title="Sample Slide Show"
              date="Date of publication"
              author="Yours Truly"
              >
            </slideshow> 

          在創建標簽或屬性的名字時,除了字符和數字之外還可以使用連字符 ("-"),下劃線("_"),冒號 (":")和句點 (".") 。和HTML不同, XML屬性的值通常在引號之內,并且不用逗號分隔多個屬性。


          注意: 要慎重使用冒號或避免一起使用,因為在定義XML文檔的命名空間的時候也要使用它。


          添加嵌套元素

          XML能夠表示層次結構化數據,這意味著一個元素可以包含其他元素。添加下面突出顯示的文本,定義一個幻燈片(slide)元素,并在它內部包含一個標題(title)元素:

          <slideshow 
            ...
            >
           
             <!-- TITLE SLIDE -->
            <slide type="all">
              <title>Wake up to WonderWidgets!</title>
            </slide>
           
          </slideshow> 

          這里,也給幻燈片(slide)添加了一個type 屬性。該屬性主要是用來標識幻燈片,是type="tech" 還是 type="exec",以便區別觀眾是技術人員還是管理人員,如果兩種類型的觀眾都有那么就是使用type="all"

          然而,更重要的是,該例子顯示了適合定義成元素(title 元素)和適合作為屬性 (type 屬性)的事務之間的區別。這里主要使用可視化啟發法。標題是觀眾能夠看到的內容。所以它是一個元素。而類型是永遠都不會顯示出來的,所以它是屬性。另一種區分方法是,元素是容器, 就像瓶子一樣。類型是容器的特征(高的還是矮的,寬的還是窄的)。標題是內容的特征 (水、牛奶還是茶)。當然,這些都不是非常嚴格的規則,但是在設計自己的XML結構的時候,它們很有用。

          添加 HTML-風格文本

          由于XML允許你定義任何你想定義的標簽,因此可以定義一組看上去類似于HTML的標簽。實際上這是通過XHTML標準實現的。在SAX教程的結束部分,你會進一步了解它。現在,輸入下面突出顯示的文本,定義一個具有兩個列表項目的幻燈片,這些項目使用HTML-風格的<em>標簽進行強調 (通常使用斜體字):

            ...
            <!-- TITLE SLIDE -->
            <slide type="all">
              <title>Wake up to WonderWidgets!</title>
            </slide>
           
            <!-- OVERVIEW -->
            <slide type="all">
              <title>Overview</title>
                <item>Why <em>WonderWidgets</em> are great</item>
                <item>Who <em>buys</em> WonderWidgets</item>
            </slide>
           
          </slideshow> 

          后面會看到,如果XHTML元素使用了跟定義的title 元素相同的名字,兩者就會發生沖突。在講解解析參數化的DTD時,會具體討論沖突產生機制 (DTD)和一些可用的解決方案。

          添加空元素

          HTML和XML的一個主要區別在于,所有的XML必須是形式良好的(well-formed)——這意味著每個標簽必須有結束標簽或為空標簽。現在,你會很滿意于使用結束標簽。添加下面突出顯示的文本,定義一個沒有內容的空列表項元素:

            ...
            <!-- OVERVIEW -->
            <slide type="all">
              <title>Overview</title>
              <item>Why <em>WonderWidgets</em> are great</item>
              <item/>
              <item>Who <em>buys</em> WonderWidgets</item>
            </slide>
           
          </slideshow> 

          注意,任何元素都可以是空元素。它所做的是用"/>" 而不是">"來結束標簽。輸入<item></item>可以實現相同的功能,它們是等價的。


          注意:使得XML形式良好的另一個因素是合適的嵌套。所以 <b><i>some_text</i></b>結構良好,因為<i>...</i> 序列完全在<b>..</b> 標簽內部。而下面序列的結構就不好: <b><i>some_text</b></i>


          最終產品

          這是XML文件的一個完整的版本:

          <?xml version='1.0' encoding='utf-8'?>
           
          <!--  A SAMPLE set of slides  --> 
          <slideshow 
            title="Sample Slide Show"
            date="Date of publication"
            author="Yours Truly"
            >
           
            <!-- TITLE SLIDE -->
            <slide type="all">
              <title>Wake up to WonderWidgets!</title>
            </slide>
           
            <!-- OVERVIEW -->
            <slide type="all">
              <title>Overview</title>
              <item>Why <em>WonderWidgets</em> are great</item>
              <item/>
              <item>Who <em>buys</em> WonderWidgets</item>
            </slide
          </slideshow> 

          現在已經創建了一個可以使用的文件,下面準備編寫程序以使用SAX解析器回送它。在下一節完成該工作。

          使用SAX解析器回送XML文件

          在實際應用中,沒有什么必要使用SAX解析器回送XML文件。通常,希望通過某種方式處理數據,以便能夠有效地利用它。(如果想要回送它,可以建立DOM樹,并用它來輸出結果。)但是,回送XML結構是查看運行中的SAX解析器的很好的方法,而且它在調試中也特別有用。

          在本練習中,將把SAX解析器事件回送到System.out。 請仔細查看XML處理程序的“Hello World”版本。它告訴你如何使用SAX解析器得到數據,然后回送它以向你展示你究竟得到了什么。


          注意:本節討論的代碼在Echo01.java中。它使用的文件是slideSample01.xml。(可瀏覽的版本是slideSample01-xml.html)。


          創建框架

          首先創建文件Echo.java,并輸入該應用程序的框架:

          public class Echo
          {
            public static void main(String argv[])
            { 
               } 
          } 

          由于準備單獨運行它,所以需要一個main方法。并且需要命令行參數,這樣就能告訴應用程序回送哪個文件。

          導入類

          接著,為應用程序使用的類的添加導入語句:

          import java.io.*;
          import org.xml.sax.*;
          import org.xml.sax.helpers.DefaultHandler;
          import javax.xml.parsers.SAXParserFactory; 
          import javax.xml.parsers.ParserConfigurationException;
          import javax.xml.parsers.SAXParser; 
          public class Echo
          {
            ... 

          當然, java.io中的類主要用來輸出。org.xml.sax 包定義了SAX 解析器使用的所有接口。SAXParserFactory 類創建了我們使用的實例。如果不能產生滿足特定參數配置的解析器,就拋出ParserConfigurationException 。(后面會具體介紹參數配置)。 SAXParser 是返回的用于解析的東西,并且DefaultHandler 定義將要處理解析器產生的SAX事件的類。

          設置I/O

          業務的第一件事是處理命令行參數,得到要回送的文件的名字,并建立輸出流。添加下面突出顯示的文本,以處理這些任務并且做一些內務工作:

          public static void main(String argv[])
           
          {
            if (argv.length != 1) {
              System.err.println("Usage: cmd filename");
              System.exit(1);
            }
            try {
              // 
              out = new OutputStreamWriter(System.out, "UTF8");
            } 
            catch (Throwable t) {
                t.printStackTrace();
            }
            System.exit(0);
          }
           
          static private Writer out; 

          創建輸出流書寫器時,選擇的是UTF-8字符編碼。也可以選擇US-ASCII或UTF-16,Java平臺也支持它們。如果想獲得關于這些字符集的更多信息,請查看Java編碼方案。

          實現ContentHandler接口

          滿足我們當前需要的最重要的接口是ContentHandler 接口。該接口需要大量方法,其中SAX解析器在響應各種解析事件時調用這些方法。處理事件的主要方法是: startDocument endDocument,、startElementendElementcharacters

          實現該接口的最簡單的方法是擴展DefaultHandler 類,該類定義在org.xml.sax.helpers 包中。該類為所有的ContentHandler 事件提供了不完成任何實際工作的方法。輸入下面突出顯示的代碼,擴展該類:

          public class Echo extends DefaultHandler
          {
            ...
          } 

          注意:DefaultHandler 也為其他定義在DTDHandler EntityResolverErrorHandler 接口中的主要事件定義不完成任何工作的方法。下面,你會進一步了解這些方法。

           

          接口需要所有的這些方法來拋出SAXException。這里拋出的異常會送給解析器,然后解析器再將其發送給調用解析器的代碼。在目前的程序中,這意味著它在main方法底部的Throwable異常處理程序中結束。

          遇到起始標簽或結束標簽時,將標簽的名字作為String 傳遞給startElementendElement 方法。遇到起始標簽時,也通過Attributes列表傳遞它定義的所有屬性。將元素內部找到的字符作為字符數組傳遞,同時傳遞字符數目(length) 和指向第一個字符的數組中的偏移量。

          設置解析器

          現在(終于)要設置解析器了。添加下面突出顯示的文本,設置并啟動它:

          public static void main(String argv[])
          {
            if (argv.length != 1) {
              System.err.println("Usage: cmd filename");
              System.exit(1);
            } 
                  // 
            DefaultHandler handler = new Echo(); 
                  //  SAXParserFactory factory = SAXParserFactory.newInstance();
            try {
              //    out = new OutputStreamWriter(System.out, "UTF8"); 
               //    SAXParser saxParser = factory.newSAXParser();
              saxParser.parse( new File(argv[0]), handler ); 
                  } catch (Throwable t) {
              t.printStackTrace();
            }
            System.exit(0);
          } 

          通過上面幾行代碼,創建了一個SAXParserFactory 實例,其中該實例由javax.xml.parsers.SAXParserFactory 系統屬性的設置決定。然后從庫中取出一個解析器,給解析器一個該類的實例來處理解析事件,以告訴它處理哪個輸入文件。


          注意: javax.xml.parsers.SAXParser 類是一個定義了大量方便方法的包裝器。它包裝了(有些不太友好) org.xml.sax.Parser 對象。如果需要,可以使用SAXParser getParser() 方法得到解析器。


          這時,你只是在簡單地捕獲解析器可能拋出的任何異常。在教程的使用非驗證解析器處理錯誤一節中將會了解到錯誤處理的更多信息。

          編寫輸出

          ContentHandler 方法拋出SAXExceptions 而不是IOExceptions,它可能在編寫的時候出現。SAXException 能夠包裝另一個異常,這樣就能用負責異常處理細節的方法進行輸出。添加下面突出顯示的代碼,定義一個emit 方法:

          static private Writer out; 
          private void emit(String s)
          throws SAXException
          {
            try {
              out.write(s);
              out.flush();
            } catch (IOException e) {
              throw new SAXException("I/O error", e);
            }
          }
          ... 

          調用emit時,I/O錯誤和它的標識信息一起包裝在SAXException 中。然后,將該異常拋回給SAX解析器。后面會具體介紹SAX異常。現在,記住emit 是處理字符串輸出的小方法。

          間隔輸出

          這是進行具體處理之前要建立的另一個基礎結構。添加下面突出顯示的代碼,定義一個nl() 方法,以寫出當前系統使用的行結束符:

          private void emit(String s) 
            ...
          } 
          private void nl()
          throws SAXException
          {
            String lineEnd = System.getProperty("line.separator");
            try {
              out.write(lineEnd);
            } catch (IOException e) {
              throw new SAXException("I/O error", e);
            }
          } 

          注意: 雖然這看起來有點多余,但還需要在前面的代碼中多次調用nl()。現在定義它可以簡化以后的代碼。它也為該教程后面部分縮排輸出結果提供了一個地方。


          處理內容事件

          最后,編寫一些實際處理ContentHandler 事件的代碼。

          文檔事件

          將下面突出顯示的代碼添加到start-document 和end-document 事件中:

          static private Writer out;
           
          public void startDocument()
          throws SAXException
          {
            emit("<?xml version='1.0' encoding='UTF-8'?>");
            nl();
          }
           
          public void endDocument()
          throws SAXException
          {
            try {
              nl();
              out.flush();
            } catch (IOException e) {
              throw new SAXException("I/O error", e);
            }
          } 
          private void echoText()
          ... 

          這里,當解析器遇到文檔開始時,回送XML聲明。由于使用UTF-8建立OutputStreamWriter ,所以將該規范作為聲明的一部分。


          注意:然而, IO類不理解帶有帶有連字符號的編碼名,所以使用“UTF8”而不用“UTF-8”。


          在文檔最后,只要放置最終新行并刷新輸出結果流。不需要進行很多操作。

          元素事件

          現在做件有趣的事情。添加下面突出顯示的代碼處理啟始元素(start-element)和結束元素(end-element)事件:

          public void startElement(String namespaceURI,
                  String sName, // simple name
                  String qName, // qualified name
                  Attributes attrs)
          throws SAXException
          {
            String eName = sName; // element name
            if ("".equals(eName)) eName = qName; // not namespaceAware
            emit("<"+eName);
            if (attrs != null) {
              for (int i = 0; i < attrs.getLength(); i++) {
                String aName = attrs.getLocalName(i); // Attr name
                if ("".equals(aName)) aName = attrs.getQName(i);
                emit(" ");
                emit(aName+"=\""+attrs.getValue(i)+"\"");
              }
            }
            emit(">");
          } 
          public void endElement(String namespaceURI,
                  String sName, // simple name
                  String qName  // qualified name
                  )
          throws SAXException
          {
            String eName = sName; // element name
            if ("".equals(eName)) eName = qName; // not namespaceAware
            emit("<"+eName+">");
          } 
          private void emit(String s)
          ... 

          使用該代碼,回送元素標簽,包括在起始標簽中定義的任何屬性。注意調用startElement() 方法時,如果不能進行命名空間處理,元素和屬性的簡稱 ("local name") 可能變成空字符串。當簡稱為空字符串時,代碼就使用限定名。

          字符事件

          要完成內容事件的處理,必須處理解析器傳遞給應用程序的字符。

          不要求解析器同時返回所有特定字符。解析器一次可以返回單個字符,也可以返回幾千個字符,但實現中仍然要遵守標準。所以,如果應用程序要處理遇到的字符,最好將這些字符積累在緩沖區中,并且僅在你認為已經找出了所有的字符時才處理它們。

          添加下面的代碼定義文本緩沖區:

          public class Echo01 extends DefaultHandler
          {
            StringBuffer textBuffer;
           
            public static void main(String argv[])
            { 
          ... 

          然后,添加下面突出顯示的代碼,積累解析器傳遞到緩沖中的字符:

          public void endElement(...)
          throws SAXException
          {
            ...
          } 
          public void characters(char buf[], int offset, int len)
          throws SAXException
          {
            String s = new String(buf, offset, len);
            if (textBuffer == null) {
              textBuffer = new StringBuffer(s);
            } else {
              textBuffer.append(s);
            }
          } 
          private void emit(String s)
          ... 

          接著,添加下面突出顯示的方法,將緩沖中的內容發送到輸出流中。

          public void characters(char buf[], int offset, int len)
          throws SAXException
          {
            ...
          } 
          private void echoText()
          throws SAXException
          {
            if (textBuffer == null) return;
            String s = ""+textBuffer
            emit(s);
            textBuffer = null;
          } 
          private void emit(String s)
          ... 

          當同一行兩次調用該方法(下面可以看到,這種情況經常發生),緩沖區為空。在這種情況下,方法僅僅返回。然而,如果緩沖區非空,將它的內容發送到輸出流中。

          最后,添加下面的代碼,以便能在開始或結束一個元素的時候,回送緩沖區的內容:

          public void startElement(...)
          throws SAXException
          {
            echoText();
            String eName = sName; // element name
            ...
          } 
          public void endElement(...)
          throws SAXException
          {
            echoText();
            String eName = sName; // element name
            ...
          } 

          當然,元素結束時,也完成了文本積累。所以,這時就可以回送了,以便在開始下一個元素之前清除緩沖區。

          但是,在開始一個元素的時候,也希望回送積累的文本。這對于文檔風格的數據來說很有必要,這種類型的數據包含混雜了文本的XML元素。例如,在該文檔段中:

          <para>This paragraph contains <bold>important</bold> 
          ideas.</para> 

          初始文本“This paragraph contains”被元素<bold> 的開始所中斷。文本“important”被結束標簽</bold>中斷,并且最后的文本“ideas”被結束標簽</para>中斷。


          注意:在大多數時候,出現endElement() 事件的時候才回送積累的文本。當這之后出現startElement() 事件時,緩沖將為空。echoText() 方法中的第一行進行該檢查,然后返回。


          恭喜!這時你已經編寫了一個完整的SAX解析器應用程序。下一步是編譯并運行它。


          注意:為了確保精確度,字符處理程序必須掃描緩沖區看看有沒有字符('&');左尖括號('<'),并且使用字符串 "&amp;" or "&lt;"替換它們。在替換和插入文本中的實體引用討論中,會獲得該類處理的更多信息。

          編譯并運行程序

          在Java WSDP中,JAXP庫發布在目錄<JWSDP_HOME>/common/lib下。要編譯創建的程序,首先需要在合適的位置安裝JAXP JAR 文件。(JAR的文件名取決于使用的JAXP版本,位置取決于使用的Java平臺的版本。查看Java XML 發行注意事項<JWSDP_HOME>/docs/jaxp/ReleaseNotes.html 獲取最新詳細信息)。


          注意:由于將JAXP 1.1 建立在Java 2平臺的1.4版本中,你也可以執行大多數的JAXP 教程部分(SAX、 DOM和XSLT),不需要安裝特殊的JAR 文件。然而,要使用JAXP中的新性能—— XML模式和XSLTC編譯轉換器——需要按照發行注意事項中的說明安裝JAXP 1.2。


          在Java 2平臺的版本1.2和1.3中,可以執行下列命令編譯并運行程序:

          javac -classpath jaxp-jar-files Echo.java
          java -cp jaxp-jar-files Echo slideSample.xml 

          或者,也可以將JAR文件放置在平臺擴展目錄下,并使用更加簡單的命令:

          javac Echo.java
          java Echo slideSample.xml 

          在Java 2平臺的1.4版本中,必須將JAR文件視作建立在Java 2平臺上的“支持標準”(endorsed standard)的新版本。這就需要將JAR文件放在支持標準目錄jre/lib/endorsed中。(復制除了jaxp-api.jar. 之外所有的ARJ文件。忽略該文件是因為JAXP API早就建立在1.4平臺中。)

          然后可以使用下面的命令編譯并運行程序:

          javac Echo.java
          java Echo slideSample.xml 

          注意: 也可以精心在命令行上設置java.endorsed.dirs 系統,讓它指向一個包含必要JAR文件的目錄,使用類似于下面的命令行參數: -D"java.endorsed.dirs=somePath".

          檢查輸出

          下面是程序輸出的一部分,顯示了一些奇怪的空白:

          ...
          <slideshow title="Sample Slide Show" date="Date of publication" 
          author="Yours Truly">
           
           
            <slide type="all">
              <title>Wake up to WonderWidgets!</title>
            </slide>
            ... 

          注意:程序的輸出保存在Echo01-01.txt中。(可瀏覽的版本是Echo01-01.html.)


          查看該輸出,會發現大量問題。就是多余的垂直空白是哪里來的?并且為什么代碼沒有進行處理,元素就能夠很好地縮進?稍后就來回答這些問題。首先,需要注意結果的一些方面:

          ·   在文件頂部定義的注釋<!-- A SAMPLE set of slides -->沒有出現在列表中。忽略注釋,除非實現LexicalHandler。在本教程的后面部分會具體介紹。

          ·   所有的元素屬性都列在一行中。如果窗口不夠寬,就看不到所有內容。

          ·   定義的單標簽空元素 (<item/>) 和雙標簽空元素(<item></item>)的處理方法相同。它們的目標和意圖是一樣的。(僅僅是更加容易輸入并需要較少的空間)

          識別事件

          該版本的回送(Echo)程序對于顯示XML非常有用,但是它沒有告訴你解析器內部究竟在做些什么。下一步是修改該程序,這樣你就能看到空白和垂直行是怎么來的。

          注意:本節討論的代碼在Echo02.java中。它的輸出結果在Echo02-01.txt中。(可瀏覽的版本是Echo02-01.html)

          執行如下代碼,以便當事件發生的時候識別它:

          public void startDocument()
          throws SAXException
          {
            nl();
            nl(); 
            emit("START DOCUMENT");
            nl(); 
            emit("<?xml version='1.0' encoding='UTF-8'?>");
            nl();
          }
           
          public void endDocument()
          throws SAXException
          {
            nl(); 
            emit("END DOCUMENT");
            try {
            ...
          }
           
          public void startElement(...)
          throws SAXException
          {
            echoText();
            nl(); 
            emit("ELEMENT: ");
            String eName = sName; // element name
            if ("".equals(eName)) eName = qName; // not namespaceAware
            emit("<"+eName);
            if (attrs != null) {
              for (int i = 0; i < attrs.getLength(); i++) {
                String aName = attrs.getLocalName(i); // Attr name
                if ("".equals(aName)) aName = attrs.getQName(i);
                emit(" ");
                emit(aName+"=\""+attrs.getValue(i)+"\"");
                nl(); 
                emit("   ATTR: ");
                emit(aName);
                emit("\t\"");
                emit(attrs.getValue(i));
                emit("\"");
              }
            }
            if (attrs.getLength() > 0) nl();
            emit(">");
          } 
          public void endElement(...)
          throws SAXException
          {
            echoText();
            nl(); 
            emit("END_ELM: ");
            String eName = sName; // element name
            if ("".equals(eName)) eName = qName; // not namespaceAware
            emit("<"+eName+">");
          }
           
          ... 
          private void echoText()
          throws SAXException
          { 
            if (textBuffer == null) return;
            nl(); 
            emit("CHARS: |"); 
            String s = ""+textBuffer
            emit(s);
            emit("|");
            textBuffer = null;
          } 

          編譯并運行該版本的程序,以輸出更多信息。現在每行顯示一個屬性。但是,更加重要的是,它輸出類似于下面的行:

            CHARS: |
           
          | 

          表明縮進空間和分隔屬性的新行來自于解析器傳遞給characters() 方法的數據。


          注意:XML規范要求所有輸入行分隔符都放在單個新行中。Java、C和UNIX系統都有自己的新行字符,但是在Windows系統中使用別名“linefeed”。


          壓縮輸出結果

          為了提高輸出可讀性,修改程序,這樣它就不會輸出空白了。

          注意:本節討論的代碼在Echo03.java中。

          做出下面的修改,壓縮所有空白輸出字符:

          public void echoText()
          throws SAXException
          {
            nl(); 
            emit("CHARS: |");
            emit("CHARS:   ");
            String s = ""+textBuffer;
            if (!s.trim().equals("")) emit(s);
            emit("|");
          } 

          然后,添加如下代碼,顯示解析器發送的每個字符集。

          public void characters(char buf[], int offset, int len)
          throws SAXException
          {
            if (textBuffer != null) {
              echoText();
              textBuffer = null;
            }
            String s = new String(buf, offset, len);
            ...
          } 

          如果現在運行程序,你可以發現你已經消除了縮進,這是因為縮進的空間是元素前面的空白的一部分。添加下面突出顯示的代碼管理縮進:

          static private Writer out; 
          private String indentString = "    "; // Amount to indent 
          private int indentLevel = 0;
           
          ... 
          public void startElement(...)
          throws SAXException
          {
            indentLevel++;
            nl(); 
            emit("ELEMENT: ");
            ...
          } 
          public void endElement(...)
          throws SAXException
          {
            nl(); 
            emit("END_ELM: ");
            emit("</"+sName+">");
            indentLevel--;
          }
          ...
          private void nl()
          throws SAXException
          {
            ...
            try {
              out.write(lineEnd);
              for (int i=0; i < indentLevel; i++)
                out.write(indentString);
            } catch (IOException e) {
            ... 
          } 

          該代碼建立縮進字符串,跟蹤當前的縮進層,并且在調用n1方法的時候,輸出縮進字符串。如果將縮進字符串設置為"",輸出結果就不縮進 (試驗一下,你會明白為什么需要添加縮進)

          當你知道你已經看到了添加到回送(Echo)程序的“機械”代碼的最后部分,你會覺得很開心。從這里開始,你所作的事情都會進一步幫你理解解析器的工作原理。目前所進行的步驟,告訴了你解析器是如何查看它處理的XML數據的。它也給你提供了一個有效的調試工具,幫助你理解解析器看到的內容。

          檢查輸出

          下面是該版本程序的部分輸出:

          ELEMENT: <slideshow
          ...
          >
          CHARS: 
          CHARS: 
            ELEMENT: <slide
            ... 
            END_ELM: </slide>
          CHARS: 
          CHARS:    

          注意:完整的輸出結果在Echo03-01.txt(可瀏覽的版本是Echo03-01.html)

          注意, 同一行兩次調用characters方法。檢查源代碼文件slideSample01.xml ,它表明在第一個幻燈片之前有注釋。第一個characters 調用出現在注釋之前。第二個調用出現在注釋之后。 (以后,你會明白當解析器遇到注釋的時候,它是如何通知你的,雖然,在多數時候你不需要這樣的通知)

          同樣也要注意, characters 方法是在第一個幻燈片之后調用的,也可以在這之前調用。在考慮分層結構化數據的時候,這看起來很多余。說到底,你希望slideshow 元素包含slide 元素,而不是文本。以后,你會知道如何使用DTD限制slideshow 元素。在這樣做的時候,就不會再調用characters 方法。

          然而,沒有DTD時,解析器必須假設它能看到的所有元素包含文本,類似于概述幻燈片的第一項元素:

          <item>Why <em>WonderWidgets</em> are great</item> 

          層次結構如下所示:

          ELEMENT:        <item>
          CHARS:        Why 
            ELEMENT:         <em>
            CHARS:         WonderWidgets
            END_ELM:        </em>
          CHARS:         are great
          END_ELM:        </item> 

          文檔和數據

          在本例中,很明顯有些字符結合了元素的層次結構。文本可以包圍元素這個事實(或避免在DTD或模式中這樣做)能夠解釋為什么有的時候聽到別人討論"XML數據"而在其他時候則聽到別人討論"XML文檔"。XML能夠處理結構化數據和包含標簽的文本文檔。兩者之間的區別在于元素之間是不是允許有文本。


          注意:在本教程的下面部分,要使用ContentHandler 接口中的ignorableWhitespace 方法。只有有DTD的時候才能調用該方法。如果DTD規定slideshow 不能包含文本,那么忽略定義中slide 元素旁邊的所有空白。另外,如果slideshow 可以包含文本(在缺少DTD的時候必須為true),那么解析器必須假設看到的slide 元素之間的空白和行是文檔的重要部分。

          添加其他事件處理程序

          ignorableWhitespace之外,可以發現即便是在非常簡單的程序中也有其他兩個ContentHandler方法:setDocumentLocatorprocessingInstruction 本節主要介紹如何實現這兩個事件處理程序。

          識別文檔的位置

          locator 是一個對象,它包含了查找文檔所必需的信息。Locator 類包含了一個系統ID (URL)或公共標識符(URN)或兩者都有。如果想要查找跟當前文檔相關的一些東西就必需該信息——例如, HTML瀏覽器處理定位標簽href="anotherFile" 的屬性——瀏覽器使用當前文檔的路徑來查找anotherFile

          也可以使用定位器打印診斷信息。除了文檔的位置和公共標識符外,定位器包含給出最近使用的列和行的數目的方法。但是,僅在解析器的開始部分調用一次setDocumentLocator 方法。要獲得當前行或列的編碼,必須要在調用setDocumentLocator 時保存定位器,然后在其他事件處理方法中使用它。

          注意:本節討論的代碼在Echo04.java中,它的輸出結果在Echo04-01.txt中。(可瀏覽的版本是Echo04-01.html.)

          首先刪除最后一個例子中添加的額外字符回送代碼:

          public void characters(char buf[], int offset, int len)
          throws SAXException
          {
            if (textBuffer != null) {
              echoText();
              textBuffer = null;
            }
            String s = new String(buf, offset, len);
            ...
          } 

          然后,將下面的方法添加到Echo程序中,獲得文檔定位器,并使用它回送文檔的系統ID。

          ...
          private String indentString = "    "; // Amount to indent
          private int indentLevel = 0; 
          public void setDocumentLocator(Locator l)
          {
            try {
              out.write("LOCATOR");
              out.write("SYS ID: " + l.getSystemId() );
              out.flush();
            } catch (IOException e) {
              // Ignore errors
            }
          } 
          public void startDocument()
          ... 

          注意:

          ·   同其他ContentHandler 方法比較,該方法并不返回SAXException。所以,不使用emit 輸出結果,該代碼將輸出直接寫到System.out。(該方法只是保存Locator 以作將來使用,在這里不產生異常.)

          ·   這些方法的拼寫是"Id"不是 "ID"。所以有getSystemIdgetPublicId

          slideSample01.xml編譯和運行程序時下面的內容就是輸出結果的重要部分:

          LOCATOR
          SYS ID: file:<path>/../samples/slideSample01.xml 
          START DOCUMENT
          <?xml version='1.0' encoding='UTF-8'?>
          ... 

          這里,很明顯setDocumentLocator在開始文檔之前調用的。如果在事件處理代碼中進行了初始化,情況就可能有所不同。

          處理處理指令

          有時在XML數據中編寫特定于應用程序的處理指令很有意義。在該練習中,在slideSample.xml 文件中添加處理指令,然后修改Echo 程序來顯示它。

          注意:本節討論的代碼在Echo05.java中。 它操作的文件是slideSample02.xml 輸出結果在Echo05-02.txt中。(可瀏覽的版本是slideSample02-xml.html Echo05-02.html.)

          理解XML中可以得知,處理指令的格式是<?target data?>,其中"target"是要進行處理的目標應用程序,并且"data"是要處理的指令或信息。添加下面的文本,為虛構幻燈片播放程序添加處理指令,該程序要求用戶找出顯示哪個幻燈片(技術層、執行層或兩者都有):

          <slideshow 
            ...
            > 
            <!-- PROCESSING INSTRUCTION -->
            <?my.presentation.Program QUERY="exec, tech, all"?> 
            <!-- TITLE SLIDE --> 

          注意:

          ·   處理指令的"data"部分可以包含空格也能為空。但是在初始<? 和目標標識符中不能有任何空格。

          ·   數據位于第一個空格之后。

          ·   利用完整的Web-unique包前綴來完全限定目標很有意義,所以避免了它跟其他可能處理相同數據的程序沖突。

          ·    為了提高可讀性,最好在應用程序名后加一個冒號(:),如下所示:

          <?my.presentation.Program: QUERY="..."?> 

          該冒號使得目標名成為"label",它識別指令的目標接收者。然而,w3c規范允許目標文件中有":" ,但是IE5的一些版本卻不支持,會出錯。本教程避免在目標名中使用冒號。

          現在可以使用一個處理指令,將下面的代碼添加到Echo應用程序中:

          public void characters(char buf[], int offset, int len)
          ...
          } 
          public void processingInstruction(String target, String data)
          throws SAXException
          {
            nl(); 
            emit("PROCESS: ");
            emit("<?"+target+" "+data+"?>");
          } 
          private void echoText()
          ... 

          完成編輯后,編寫并運行程序。輸出結果的相關部分看起來如下所示:

          ELEMENT: <slideshow
            ...
          >
          PROCESS: <?my.presentation.Program QUERY="exec, tech, all"?>
          CHARS: 
          ... 

          小結

          在小異常ignorableWhitespace中,你已經使用了大部分的ContentHandler 方法來處理最通用的SAX 事件。后面會介紹ignorableWhitespace 。然后,將深入了解如何在SAX解析處理中處理錯誤。

          使用非驗證解析器處理錯誤

          該版本的Echo程序使用非驗證解析器。所以它不能確定XML文檔是否包含了正確的標簽,或這些標簽是否在正確的序列中。換句話說,它不能告訴你文檔是否有效。然而,它能判斷出文檔結構是否良好。

          本節中,要修改幻燈片顯示(slideshow)文件,以生成各類錯誤,并觀察解析器是如何處理它們的。你能夠看出默認情況下哪些錯誤情形是可以忽略的,也可以看到如何處理它們的。

          生成錯誤

          解析器可以產生三類錯誤:致命錯誤、錯誤和警告。在本練習中,我們將修改XML文件以產生一個致命錯誤。然后查看Echo應用程序是如何處理它的。

          注意:本練習中要創建的XML結構在slideSampleBad1.xml中。輸出結果在Echo05-Bad1.txt中。 (可瀏覽版本是slideSampleBad1-xml.html Echo05-Bad1.html.)

          產生致命錯誤的一個最簡單的方法就是刪除空item 元素最后的"/" ,創建一個沒有相應結束標簽的標簽。這構成了一個致命錯誤,因為根據定義,所有的XML 文檔必須具有良好的結構。步驟如下:

          1. 將slideSample.xml 復制到 badSample.xml

          2. 編輯badSample.xml 并且刪除下面的字符:

          ...
          <!-- OVERVIEW -->
          <slide type="all">
            <title>Overview</title>
            <item>Why <em>WonderWidgets</em> are great</item>
            <item/>
            <item>Who <em>buys</em> WonderWidgets</item>
          </slide>
          ... 

          產生:

          ...
          <item>Why <em>WonderWidgets</em> are great</item>
          <item>
          <item>Who <em>buys</em> WonderWidgets</item> 
          ... 

          1. 在新文件上運行Echo程序。

          輸出結果給出如下錯誤消息:

          org.xml.sax.SAXParseException: 
            The element type "item" must be terminated by the
            matching end-tag "</item>".
          ...
          at org.apache.xerces.parsers.AbstractSAXParser...
          ...
          at Echo.main(...) 

          注意:上面的代碼是JAXP 1.2 庫產生的。如果使用不同的解析器,錯誤消息可能會有所不同。

          當出現致命錯誤時,解析器就不能繼續下去。所以,如果應用程序沒有產生異常(一會兒你就會看到怎么做了),那么默認的錯誤-事件處理程序會產生一個致命錯誤。棧跟蹤是由main方法中的Throwable 異常處理程序產生的:

              ...
          } catch (Throwable t) {
            t.printStackTrace();
          } 

          棧跟蹤不是非常有用。然后,你會看到出現錯誤時如何產生更好的診斷信息。

          處理SAXParseException

          遇到錯誤時,解析器產生SAXParseException--SAXException 的一個子類,以識別錯誤出現的文件和位置。

          注意:本練習中要創建的代碼在Echo06.java中。輸出結果在Echo06-Bad1.txt中。(可瀏覽版本是 Echo06-Bad1.html.)

          添加下面的代碼,在出現異常時產生更好的診斷信息:

          ...
          } catch (SAXParseException spe) {
            // Error generated by the parser
            System.out.println("\n** Parsing error" 
              + ", line " + spe.getLineNumber()
              + ", uri " + spe.getSystemId());
            System.out.println("   " + spe.getMessage() );
           
          } catch (Throwable t) {
            t.printStackTrace();
          } 

          這時運行程序將產生一個錯誤消息,該錯誤消息非常有用,格式如下:

          ** Parsing error, line 22, uri file:<path>/slideSampleBad1.xml
            The element type "item" must be ... 

          注意:錯誤消息的文本取決于使用的解析器。該消息是用JAXP 1.2產生的。

          注意:在產品應用程序中不適合這樣捕獲異常。現在開始逐步建立完整的錯誤處理程序。另外,它能捕獲所有的空指針異常,當傳遞給解析器空值時,會拋出該異常。

          處理SAXException

          有時解析器能夠產生一個更加通用的SAXException 實例,但是這個實例通常在應用程序的事件處理方法產生錯誤的時候出現。例如, ContentHandler 接口中的startDocument 方法的會返回一個SAXException

          public void startDocument() throws SAXException 

          所有的ContentHandler 方法 (除了 setDocumentLocator) 具有該特征定義。

          SAXException 可以使用消息、另一個異常或兩個都用。例如,當Echo.startDocument 使用emit 方法輸出一個字符串時,所有出現的I/O異常都包裝在SAXException 中并且被發送回解析器。

          private void emit(String s)
          throws SAXException
          {
            try {
              out.write(s);
              out.flush();
            } catch (IOException e) {
              throw new SAXException("I/O error", e);
            }
          } 

          注意: 如果在調用setDocumentLocator 的時候保存Locator 對象,你可以使用它來產生SAXParseException,識別文檔和位置,而不產生SAXException

          當解析器將異常發送回調用解析器的代碼,可以使用原始異常產生棧跟蹤。添加下面的代碼實現該功能:

             ...
          } catch (SAXParseException err) {
            System.out.println("\n** Parsing error" 
              + ", line " + err.getLineNumber()
              + ", uri " + err.getSystemId());
            System.out.println("   " + err.getMessage());
           
          } catch (SAXException sxe) {
            // Error generated by this application
            // (or a parser-initialization error)
            Exception  x = sxe;
            if (sxe.getException() != null)
              x = sxe.getException();
            x.printStackTrace();
           
          } catch (Throwable t) {
            t.printStackTrace();
          } 

          該代碼測試SAXException 有沒有包裝另外一個異常。如果是,它產生一個棧跟蹤,以精細化錯誤處理代碼,其中異常就是從棧跟蹤開始的。如果異常僅包含一條消息,代碼從產生異常的地方開始打印棧跟蹤。

          改進SAXParseException處理程序

          由于SAXParseException 也能包裝另一個異常,添加下面的代碼,在棧跟蹤中使用包含的異常:

              ...
          } catch (SAXParseException err) {
            System.out.println("\n** Parsing error" 
              + ", line " + err.getLineNumber()
              + ", uri " + err.getSystemId());
            System.out.println("   " + err.getMessage());
           
            // Use the contained exception, if any
            Exception  x = spe;
            if (spe.getException() != null)
              x = spe.getException();
            x.printStackTrace(); 
          } catch (SAXException sxe) {
            // Error generated by this application
            // (or a parser-initialization error)
            Exception      x = sxe;
            if (sxe.getException() != null)
              x = sxe.getException();
            x.printStackTrace(); 
          } catch (Throwable t) {
            t.printStackTrace();
          }       

          現在程序要處理它看到的任何SAX解析異常。可以看到解析器產生致命錯誤的異常。但是對于非致命性錯誤和警告,默認的錯誤處理程序不會產生異常,也不會顯示任何消息。下面,學進一步學習錯誤和警告,并查看如何提供錯誤處理程序處理它們。

          處理 ParserConfigurationException

          最后,回想一下,如果類SAXParserFactory不能創建一個解析器就會拋出異常。如果factory不能找到創建解析器(類不能找到錯誤)的類就會產生這樣的錯誤,即不允許訪問它 (非法訪問異常)或不能將它實例化(實例化錯誤)。

          添加下面的代碼處理這類錯誤:

          } catch (SAXException sxe) {
            Exception      x = sxe;
            if (sxe.getException() != null)
              x = sxe.getException();
            x.printStackTrace();
           
          } catch (ParserConfigurationException pce) {
            // Parser with specified options can't be built
            pce.printStackTrace();
           
          } catch (Throwable t) {
            t.printStackTrace(); 

          不可否認,這里有相當多的錯誤處理程序。但是現在你至少知道了可能出現的異常類型。

          注意:如果不能找到系統屬性指定的factory類或不能將它實例化,也可拋出javax.xml.parsers.FactoryConfigurationError 。這是一個非陷阱錯誤(non-trappable error),程序不能恢復它。

          處理 IOException

          最后,添加IOExceptions的處理程序:

          } catch (ParserConfigurationException pce) {
            // Parser with specified options can't be built
            pce.printStackTrace();
           
          } catch (IOException ioe) {
            // I/O error
            ioe.printStackTrace();
          }
           
          } catch (Throwable t) {
            ... 

          Throwables 的處理程序捕捉空指針錯誤,但是注意,在這點上它跟IOException 處理程序相同。這里,僅僅顯示可能出現的例外,以免其中的一些你的應用程序能夠恢復。

          處理非致命錯誤

          當XML文檔不能滿足有效性約束的時候,會出現非致命 錯誤。如果解析器發現文檔無效,就會產生一個錯誤事件。給定一個DTD 或模式,當文檔具有無效標簽,或標簽出現在不應該出現的地方,或元素包含無效數據(模式的情況中),驗證解析器就會產生該錯誤。

          實際上到現在為止還沒有涉及到有效性問題。但是,由于現在討論的是錯誤處理這一主題,所以現在可以編寫錯誤處理代碼。

          理解非致命性錯誤的最重要的原則是在默認情況下,它們是可以忽略的。

          但是如果在文檔中出現有效性錯誤,你可能不樂意繼續處理它。你可能希望將它們作為致命錯誤處理。在下面編寫的代碼中,建立錯誤處理程序實現該功能。

          注意:本練習中創建的程序的代碼在Echo07.java 中。

          為了接管錯誤處理,覆蓋方法DefaultHandler ,該方法將致命錯誤、非致命錯誤和警告作為ErrorHandler 接口的一部分處理。SAX 解析器將SAXParseException 發送給這些方法,所以當出現錯誤的時候,只要拋回它就能產生錯誤。

          添加下面的代碼,覆蓋錯誤處理程序:

          public void processingInstruction(String target, String data)
          throws SAXException
          {
            ...
          } 
          // treat validation errors as fatal
          public void error(SAXParseException e)
          throws SAXParseException
          {
            throw e;
          } 

          注意:查看org.xml.sax.helpers.DefaultHandler中定義的錯誤處理方法很有意義。可以看到error()warning() 方法沒有做什么事情,而fatalError() 拋出一個異常。當然,你可以覆蓋fatalError() 方法拋出不同的異常。但是當出現致命錯誤的時候,你的程序不能拋出一個異常,然后SAX解析器將會拋出一個異常——XML規范需要該異常。

          處理警告

          默認情況下,警告也被忽略。警告能提供很多信息,并且需要一個DTD。例如,如果在DTD中兩次定義了一個元素,產生一個警告——它并不是不合法的,并且不會引起任何問題,但是你可能希望知道這些,因為它可能不是故意的。

          添加下面的代碼,以便在出現警告時生成消息:

          // treat validation errors as fatal
          public void error(SAXParseException e)
          throws SAXParseException
          {
            throw e;
          } 
          // dump warnings too
          public void warning(SAXParseException err)
          throws SAXParseException
          {
            System.out.println("** Warning"
              + ", line " + err.getLineNumber()
              + ", uri " + err.getSystemId());
            System.out.println("   " + err.getMessage());
          } 

          由于沒有DTD或模式的情況下很難產生警告, 所以你現在還看不到任何警告。但是如果出現了警告,你也能處理。

          posted on 2005-03-23 15:19 閱讀(303) 評論(0)  編輯  收藏 所屬分類: Java_Xml

          主站蜘蛛池模板: 扶绥县| 邵武市| 松阳县| 桃江县| 丰台区| 社旗县| 伊宁市| 牙克石市| 亳州市| 都江堰市| 孝义市| 永济市| 万州区| 琼海市| 石城县| 鹤岗市| 新绛县| 余庆县| 九江市| 沽源县| 柯坪县| 武城县| 赤峰市| 定边县| 高安市| 喀什市| 徐州市| 常德市| 金坛市| 中牟县| 平罗县| 莆田市| 凤庆县| 太谷县| 遂川县| 秭归县| 慈利县| 泗水县| 南部县| 醴陵市| 惠东县|