碰巧,你在過(guò)去可能已經(jīng)用過(guò)眾多Java庫(kù)中的某一個(gè)以操作XML數(shù)據(jù)結(jié)構(gòu)。那么JDOM(Java文檔對(duì)象模型,Java Document Object Model)的用途是什么?開(kāi)發(fā)者又為什么需要它呢?
JDOM是用于Java優(yōu)化的XML數(shù)據(jù)操作的源代碼開(kāi)放的Java庫(kù)。盡管它與萬(wàn)維網(wǎng)協(xié)會(huì)(W3C,World Wide Web Consortium)的DOM類(lèi)似,但它是一種可供選擇的文檔對(duì)象模型,它不是在DOM基礎(chǔ)上構(gòu)建的,也不以DOM為模型。它們的主要區(qū)別是:DOM是與語(yǔ)言無(wú)關(guān)的,最初是用于HTML頁(yè)的JavaScript操作,而JDOM專(zhuān)用于Java,因此它利用Java的特性,其中包括方法過(guò)載(method overloading)、集合(collection)、映像(reflection)和人們熟悉的編程風(fēng)格等。對(duì)Java編程人員來(lái)說(shuō),JDOM顯得更自然、更"適用"。就像Java優(yōu)化的RMI(remote method invocation,遠(yuǎn)程方法調(diào)用)庫(kù)要比與語(yǔ)言無(wú)關(guān)的CORBA(Common Object Request Broker Architecture,通用對(duì)象申請(qǐng)中介體系結(jié)構(gòu))更自然一樣。
你可以根據(jù)源代碼開(kāi)放的Apache類(lèi)型(商業(yè)友好)的許可,在jdom.org找到JDOM。它是合作設(shè)計(jì)開(kāi)發(fā)的,并擁有超過(guò)3000個(gè)訂閱者的郵件列表。此庫(kù)已經(jīng)被Sun公司的Java社區(qū)進(jìn)程(JCP,Java Community Process)接受作為Java規(guī)范申請(qǐng)(JSR-102),并很快成為正式Java規(guī)范。
本系列文章將對(duì)JDOM進(jìn)行技術(shù)介紹。本文提供一些關(guān)于重要類(lèi)的信息。下一篇文章將介紹如何在自己的Java程序中應(yīng)用JDOM。
ORACLE XML工具
XML Developer Kit(XDK)是Oracle為開(kāi)發(fā)人員提供的免費(fèi)XML工具庫(kù)。它包括可以與JDOM一起使用的XML分析器和XSLT轉(zhuǎn)換引擎。您可以在Oracle XML主頁(yè)oracle.com/xml上找到關(guān)于這些工具的很多信息。
要下載這個(gè)分析器,請(qǐng)查找名字為"XDK for Java"的XML Developer Kit。點(diǎn)擊下載鏈接左欄中的"Software"。將該軟件解壓縮后,文件xalparserv2.jar中即包括該分析器。
為了配置JDOM和其他軟件,以便以缺省方式使用Oracle分析器的軟件,需要將JAXP的javax.xml.parsers.SAXParserFactory系統(tǒng)屬性設(shè)置為oracle.xml.jax. JXSAXParserFactory。它告訴JAXP你選擇使用Oracle分析器。最簡(jiǎn)單的設(shè)置方法是采用命令行:
java -Djavax.xml.parsers.SAXParserFactory=
oracle.xml.jaxp.JXSAXParserFactory
也可以通過(guò)編程來(lái)設(shè)置:
System.setProperty("jaxax.xml.parsers
.SAXParserFactory",
"oracle.xml.jaxp.JXSAXParserFactory");
除了XDK之外,Oracle還在Oracle9i數(shù)據(jù)庫(kù)版本2中提供了一個(gè)本地XML知識(shí)庫(kù)。Oracle9i XML數(shù)據(jù)庫(kù)(XDB)是一個(gè)高性能、本地XML存儲(chǔ)和檢索的技術(shù)。它將W3C數(shù)據(jù)模型全面應(yīng)用到Oracle9i數(shù)據(jù)庫(kù)中,并為定位和查詢XML提供了新的標(biāo)準(zhǔn)訪問(wèn)方法。采用XDB可以充分利用關(guān)系型數(shù)據(jù)庫(kù)技術(shù)和XML技術(shù)的優(yōu)點(diǎn)。 |
JDOM軟件包結(jié)構(gòu)
JDOM庫(kù)包括六個(gè)軟件包。第一個(gè)是org.jdom包,它包括表示XML文檔以及其組件的類(lèi),這些組件有:Attribute、CDATA、Comment、DocType、Document、Element、EntityRef、Namespace、Processing Instruction和Text等。如果你對(duì)XML很熟悉,通過(guò)類(lèi)的名字就可以知道它的用途了。
下一個(gè)是org.jdom.input包,它包括用于構(gòu)建XML文檔的類(lèi)。其中最主要和最重要的類(lèi)是SAXBuilder。SAXBuilder通過(guò)監(jiān)聽(tīng)輸入的XML簡(jiǎn)單API(SAX,Simple API for XML)事件建立相應(yīng)的文檔。如果需要由一個(gè)文件或其他流(stream)建立文檔,那就可以采用SAXBuilder。它采用SAX分析器讀取流,并根據(jù)SAX分析器的"回調(diào)"建立文檔。這一設(shè)計(jì)的一個(gè)好處就是SAX分析器越快,SAXBuilder也會(huì)越快。另一個(gè)主要輸入類(lèi)是DOMBuilder。DOMBuilder由DOM樹(shù)構(gòu)建。如果預(yù)先已經(jīng)有了一個(gè)DOM樹(shù),并希望用JDOM版本的樹(shù)來(lái)代替,那么采用DOMBuilder是非常方便的。
對(duì)于這些可能的生成器來(lái)說(shuō),不存在什么限制。例如,由于Xerces有Xerces本地接口(XNI,Xerces Native Interface),可用于在比SAX較低的層次上進(jìn)行操作,所以編寫(xiě)一個(gè)XNIBuilder以支持某些分析器知識(shí),而不采用SAX,就可能很有意義。一個(gè)構(gòu)成JDOM項(xiàng)目的很流行的生成器是ResultSetBuilder。它利用JDBC結(jié)果集合,生成一個(gè)XML文檔,用以表示SQL結(jié)果,根據(jù)是哪種元素和哪種屬性,它有多種不同的編排。
org.jdom.output包中包括輸出XML文檔的一些類(lèi)。其中最重要的類(lèi)是XMLOutputter。它將文檔轉(zhuǎn)換為一個(gè)字節(jié)流,用于輸出到文件、流和接口程序(sockets)中。XMLOutputter具有很多支持原始輸出(raw output)、完美輸出(pretty output)、壓縮輸出或其他輸出的特殊配置選項(xiàng)。它是一個(gè)相當(dāng)復(fù)雜的類(lèi)。這可能是為什么DOM level2中仍然沒(méi)有這種能力的原因。
其他輸出器(outoutter)包括SAXOutputter,它可以產(chǎn)生基于文檔內(nèi)容的SAX事件。盡管它看起來(lái)有些神秘,但事實(shí)證明這個(gè)類(lèi)在XSLT變換中極其有用,這是因?yàn)橄鄬?duì)于將文檔數(shù)據(jù)傳遞到一個(gè)引擎的字節(jié)來(lái)說(shuō),SAX事件是一種更為有效的方法。還有一個(gè)DOMOutputter輸出器,它用于生成表示文檔的DOM樹(shù)。一個(gè)有趣的輸出器是JTreeOutputter,它只有幾十行代碼,它可以建立表示文檔的JTree。將它與ResultSetBuilder聯(lián)合起來(lái)后,僅用幾行代碼就可以通過(guò)SQL查詢得到結(jié)果的樹(shù)視圖。
注意,與在DOM中不同,文檔并不與它們的生成器捆綁在一起。這就形成了一個(gè)出色的模型,在這種模型中,你可以用一些類(lèi)保持?jǐn)?shù)據(jù),另一些類(lèi)構(gòu)造數(shù)據(jù),其他一些類(lèi)來(lái)定制數(shù)據(jù)。可以隨意進(jìn)行混合和匹配。
org.jdom.transform和org.jdom.xpath包中還有支持內(nèi)置的XSLT轉(zhuǎn)換和XPath查找的類(lèi)。
最后,org.jdom.adapters包中還包括在DOM交互中對(duì)庫(kù)提供幫助的類(lèi)。該庫(kù)的使用者永遠(yuǎn)不需要調(diào)用這一軟件包中的類(lèi)。這些類(lèi)是隨時(shí)可用的,因?yàn)槊總€(gè)DOM實(shí)現(xiàn)都為特定的引導(dǎo)任務(wù)生成不同的方法名稱(chēng),所以適配器類(lèi)將標(biāo)準(zhǔn)調(diào)用轉(zhuǎn)換成分析器專(zhuān)用的調(diào)用。Java API for XML Processing(JAXP)為解決此問(wèn)題提供了另一種方法,事實(shí)上降低了對(duì)這些類(lèi)的需要,但是這些類(lèi)仍然保留了下來(lái),這是因?yàn)橛捎谠S可證的原因,并不是所有的分析器都支持JAXP,也不是任何地方都安裝有JAXP。
生成文檔
文檔由org.jdom.Documentclass表示,你可以從頭建立一個(gè)文檔:
// This builds: <root/>
Document doc = new Document(new Element("root"));
也可以由文件、流、系統(tǒng)ID或URL建立一個(gè)文檔:
// This builds a document of whatever's in the given resource
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(url);
將幾個(gè)調(diào)用結(jié)合在一起,可以很容易地在JDOM中建立一個(gè)簡(jiǎn)單的文檔:
// This builds: <root>This is the root</root>
Document doc = new Document();
Element e = new Element("root");
e.setText("This is the root");
doc.addContent(e);
如果你是一位非常有才能的用戶,你可能更愿意使用"方法鏈",在這種方式中,多種方法是依次調(diào)用的。這種方式之所以有效是因?yàn)榧戏椒ǚ祷厮鼈兯饔玫膶?duì)象,下面是一個(gè)例子:
Document doc = new Document(
new Element("root").setText("This is the root"));
為了進(jìn)行對(duì)比,下面給出如何用JAXP/DOM創(chuàng)建相同的文檔:
// JAXP/DOM
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();
Element root = doc.createElement("root");
Text text = doc.createText("This is the root");
root.appendChild(text);
doc.appendChild(root);
用SAXBuilder生成文檔
如前所示,SAXBuilder提供了一種由任意面向字節(jié)的數(shù)據(jù)源來(lái)創(chuàng)建文檔的簡(jiǎn)單機(jī)制。缺省的無(wú)參數(shù)SAXBuilder()構(gòu)造器在后臺(tái)利用JAXP來(lái)選擇一個(gè)SAX分析器。如果你希望改變分析器,則可以設(shè)置javax.xml.parsers.SAXParserFactory系統(tǒng)屬性,以指向你的分析器提供的SAXParser Factory實(shí)現(xiàn)。對(duì)于Oracle9i版本2 XML分析器,應(yīng)當(dāng)這樣做:
java -Djavax.xml.parsers.SAXParserFactory=
oracle.xml.jaxp.JXSAXParserFactory YourApp
對(duì)于Xerces分析器,應(yīng)當(dāng)這樣做:
java -Djavax.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp
.SAXParserFactoryImpl YourApp
如果沒(méi)有安裝JAXP,SAXBuilder缺省地指向Apache Xerces。一旦你創(chuàng)建了SAXBuilder實(shí)例,你就可以在生成器中設(shè)置幾個(gè)屬性,其中包括:
setValidation(boolean validate)
相關(guān)鏈接
開(kāi)放的源JDOM庫(kù) jdom.org
Java Servlet編程(第二版),作者:Jason Hunter(O'Reilly及其聯(lián)合出版機(jī)構(gòu)出版,2001) www.oreilly.com |
這一方法告訴分析器在生成過(guò)程中是否根據(jù)Document Type Definition(DTD,文檔類(lèi)型定義)進(jìn)行驗(yàn)證。缺省設(shè)置為"否"(off)。所用的DTD是文檔中DocType引用的一個(gè)。根據(jù)任何其他DTD進(jìn)行驗(yàn)證是不可能的,因?yàn)檫€沒(méi)有分析器支持這一功能。
setIgnoringElementContentWhitespace(boolean ignoring)
這一方法告訴分析器是否忽略元素內(nèi)容中所謂的"可忽略空格(whitespace)"。按照XML1.0規(guī)范,元素內(nèi)容中的空格必須由分析器保留,但當(dāng)根據(jù)DTD進(jìn)行驗(yàn)證時(shí),分析器可能知道文檔的特定部分不會(huì)支持空格,所以這一區(qū)域的任何空格都是"可忽略的"。其缺省設(shè)置為"否"(off)。如果你不希望使一個(gè)文檔"往返旅行",將輸入內(nèi)容以原樣輸出的話,那么將這一開(kāi)關(guān)打開(kāi)通常會(huì)使性能略微有些提高。注意,這一標(biāo)志只有在驗(yàn)證已完成時(shí)才有效。而進(jìn)行驗(yàn)證會(huì)導(dǎo)致性能下降,所以這一技巧僅在已應(yīng)用驗(yàn)證時(shí)才有用。
setFeature(String name, String value)
這一方法設(shè)置基礎(chǔ)SAX分析器的一個(gè)特性。這是一個(gè)原始的"傳遞(pass-through)"調(diào)用,所以在應(yīng)用這一方法時(shí)應(yīng)非常小心,因?yàn)閷?duì)特性的錯(cuò)誤設(shè)置(如弄錯(cuò)名稱(chēng)空間)可能會(huì)中斷JDOM行為。而且,依靠任何特定分析器特性都會(huì)限制可移植性。這一調(diào)用對(duì)于啟用模式驗(yàn)證最為有用。
setProperty(String name, Object value)
這一方法設(shè)置基礎(chǔ)SAX分析器的一個(gè)屬性。它也是一個(gè)原始的"傳遞"調(diào)用,它具有同樣的風(fēng)險(xiǎn),而且對(duì)于有才能的用戶同樣有用,特別是對(duì)于模式驗(yàn)證。下面的代碼結(jié)合這些方法,利用JAXP選擇的分析器讀取本地文件,驗(yàn)證功能有效,可忽略的空格都被忽略。
SAXBuilder builder = new SAXBuilder();
builder.setValidation(true);
builder.setIgnoringElementContentWhitespace(true);
Document doc = builder.build(new File("/tmp/foo.xml"));
用XMLOutputter輸出
文檔可以以多種不同的格式輸出,但最常見(jiàn)的輸出格式還是字節(jié)流。在JDOM中,XMLOutputter類(lèi)提供這一能力。它的缺省無(wú)參數(shù)生成器試圖忠實(shí)地輸出一個(gè)與內(nèi)存中貯存的完全一樣的文檔。下面的代碼向一個(gè)文件生成一個(gè)文檔的原始表示。
// Raw output
XMLOutputter outp = new XMLOutputter();
outp.output(doc, fileStream);
如果你不關(guān)心空格,那么你可以對(duì)文本塊進(jìn)行整理,以節(jié)省一點(diǎn)帶寬:
// Compressed output
outp.setTextTrim(true);
outp.output(doc, socketStream);
如果希望文件打印得很漂亮,使其展示給人們看,則可以增加一些縮進(jìn)空格,并增加一些新行:
outp.setTextTrim(true);
outp.setIndent(" ");
outp.setNewlines(true);
outp.output(doc, System.out);
對(duì)于已有格式化空格的文檔,要打印得很漂亮,就一定要進(jìn)行整理。否則,就會(huì)在已經(jīng)格式的文檔上添加新的格式,使打印的文檔顯得很難看。
定位元素樹(shù)
JDOM使得元素樹(shù)的定位非常容易。為了取得根元素,可調(diào)用:
Element root = doc.getRootElement();
要得到它所有子元素的列表:
List allChildren = root.getChildren();
僅想得到具有給定名稱(chēng)的元素:
List namedChildren = root.getChildren("name");
僅想得到具有給定名稱(chēng)的第一個(gè)元素:
Element child = root.getChild("name");
getChildren()調(diào)用返回的列表是java.util.List,它是所有Java編程人員都知道的列表(List)接口的一個(gè)實(shí)現(xiàn)。這一列表令人感興趣的地方在于它是活的。該列表的任何變化都會(huì)在支持的文檔中立即反應(yīng)出來(lái)。
// Remove the fourth child
allChildren.remove(3);
// Remove children named "jack"
allChildren.removeAll(root.getChildren("jack"));
// Add a new child, at the tail or at the head
allChildren.add(new Element("jane"));
allChildren.add(0, new Element("jill"));
利用該List隱喻(List metaphor)可以不必增加過(guò)多的方法而進(jìn)行許多元素操作。但是為了方便起見(jiàn),在結(jié)尾增加元素或者刪去給定名字的元素之類(lèi)的常用任務(wù)都有涉及到元素(Element)自身的方法,而不需要首先得到該List:
root.removeChildren("jill");
root.addContent(new Element("jenny"));
采用JDOM的一個(gè)好處就是它可以很容易地在一個(gè)文檔內(nèi)或在文檔之間移動(dòng)元素。兩種情況的代碼相同,如下所示:
Element movable = new Element("movable");
parent1.addContent(movable); // place
parent1.removeContent(movable); // remove
parent2.addContent(movable); // add
采用DOM時(shí),移動(dòng)元素就沒(méi)有這么容易,因?yàn)樵贒OM中,元素是與它的生成工具緊緊聯(lián)系在一起的。所以在文檔之間移動(dòng)DOM元素時(shí),必須將其"導(dǎo)入"。
而采用JDOM時(shí),你需要記住的唯一一件事件就是在將一個(gè)元素增加到其他位置之前,需要先先將它刪除,這樣你就不會(huì)在樹(shù)中形成循環(huán)。detach()方法可以用一行代碼完成分離/增加:
parent3.addContent(movable.detach());
如果在將一個(gè)元素增加到另一個(gè)父元素之前忘了將它分離,則該庫(kù)會(huì)產(chǎn)生一個(gè)異常(帶有一個(gè)真正準(zhǔn)確而有用的錯(cuò)誤信息)。該庫(kù)還會(huì)檢查元素的名字和內(nèi)容,以確保它們不包括空格之類(lèi)的不恰當(dāng)字符。它還會(huì)驗(yàn)證其他一些規(guī)則,如只有一個(gè)根元素、名稱(chēng)空間的聲明是一致的、在注釋和CDATA部分沒(méi)有禁止使用的字符串等等。這一特性能夠盡可能早地在該進(jìn)程中進(jìn)行"格式正確性"(well-formedness)錯(cuò)誤檢查。
處理元素的屬性
元素屬性的格式如下所示:
<table width="100%" border="0"> ... </table>
利用對(duì)元素的引用,可以要求元素的任意給定屬性值:
String val = table.getAttributeValue("width");
也可以將屬性看作一個(gè)對(duì)象,用于進(jìn)行一些特殊的操作,如類(lèi)型變換等:
Attribute border = table.getAttribute("border");
int size = border.getIntValue();
要設(shè)置或改變一個(gè)屬性,可以采用setAttribute():
table.setAttribute("vspace", "0");
要?jiǎng)h除一個(gè)屬性,可以采用removeAttribute():
table.removeAttribute("vspace");
處理元素文本內(nèi)容
一個(gè)文本內(nèi)容類(lèi)似于:
<description>
A cool demo
</description>
在JDOM中,這一文本可以通過(guò)調(diào)用直接獲得:
String desc = description.getText();
要記住,因?yàn)閄ML 1.0規(guī)范要求保留空格,所以它會(huì)返回"\n A cool demo\n"。當(dāng)然,有經(jīng)驗(yàn)的編程人員常常不希望得到格式化的空格。有一個(gè)很方便的方法可以檢索文本而同時(shí)忽略其中的空格:
String betterDesc = description.getTextTrim();
如果你真地希望去掉空格,那么有一個(gè)getTextNormalize()方法可以將內(nèi)部空白成為一個(gè)標(biāo)準(zhǔn)空格。這一方法對(duì)于類(lèi)似于下面這樣的文本是非常方便的:
<description>
Sometimes you have text content with formatting
space within the string.
</description>
要改變文本內(nèi)容,可以應(yīng)用setText()方法:
description.setText("A new description");
這一文本內(nèi)的任何特殊字符都可以正確地被解釋為一個(gè)字符,并根據(jù)需要在輸出時(shí)刪除,使以保持文本的語(yǔ)法正確。比如說(shuō)你進(jìn)行了這樣一個(gè)調(diào)用:
element.setText("<xml/> content");
在內(nèi)部存儲(chǔ)中,仍然將這些字符串看作字符。不會(huì)對(duì)其內(nèi)容進(jìn)行隱式分析。在輸出中,你將看到:
<xml/> content<elt>
這一操作保留了前面setText()調(diào)用的語(yǔ)義。如果你希望在一個(gè)元素中保留XML內(nèi)容,則必須增加相應(yīng)的JDOM子元素對(duì)象。
處理CDATA節(jié)也可能在JDOM內(nèi)進(jìn)行。一個(gè)CDATA節(jié)指出不應(yīng)被分析的文本塊。它實(shí)質(zhì)上是一個(gè)"語(yǔ)法糖塊(syntactic sugar)",它允許很容易地包含HTML或XML內(nèi)容,而不需要很多<和>換碼字符。要建立一個(gè)CDATA節(jié),只需要在CDATA對(duì)象內(nèi)封裝字符串即可:
element.addContent(new CDATA("<xml/> content"));
JDOM最了不起的地方是getText()調(diào)用返回字符串時(shí),不會(huì)麻煩調(diào)用程序去判斷它是否由CDATA節(jié)表示。
處理混合內(nèi)容
一些元素包括有很多元素,如空格、注釋、文本、子元素,以及其他元素:
<table>
<!-- Some comment -->
Some text
<tr>Some child element</tr>
</table>
如果一個(gè)元素中同時(shí)包含文本和子元素,就說(shuō)它包含有"混合內(nèi)容" 。處理混合內(nèi)容可能是非常困難的,但JDOM使它變得非常容易。標(biāo)準(zhǔn)使用情況--返回文本內(nèi)容和定位子元素--都非常簡(jiǎn)單:
String text = table.getTextTrim(); // "Some text"
Element tr = table.getChild("tr"); // A straight reference
對(duì)于需要注釋、空格塊、處理指令和實(shí)體引用這樣一些更復(fù)雜的應(yīng)用來(lái)說(shuō),原始混合內(nèi)容可以作為一個(gè)List(列表)來(lái)提供:
List mixedCo = table.getContent();
Iterator itr = mixedCo.iterator();
while (itr.hasNext()) {
Object o = i.next();
if (o instanceof Comment) {
...
}
// Types include Comment, Element, CDATA, DocType,
// ProcessingInstruction, EntityRef, and Text
}
就像子元素列表一樣,對(duì)原始內(nèi)容列表的改變會(huì)影響到支持的文檔:
// Remove the Comment. It's "1" because "0" is a whitespace block.
mixedCo.remove(1);
如果你的觀察力很強(qiáng),則你會(huì)注意到這里有一個(gè)Text類(lèi)。JDOM在內(nèi)部采用Text類(lèi)存儲(chǔ)串內(nèi)容,從而允許串具有"父輩(parentage)"關(guān)系,并更容易支持XPath訪問(wèn)。作為一個(gè)編程人員,在檢索或設(shè)置文本時(shí),不需要擔(dān)心這種類(lèi),而只需要在訪問(wèn)原始內(nèi)容列表時(shí)關(guān)心它就行了。.
對(duì)于DocType、處理指令和EntityRef類(lèi)的詳細(xì)內(nèi)容,請(qǐng)參見(jiàn)jdom.org上的API文檔。
關(guān)于第2部分
在本文中,開(kāi)始研究了如何在應(yīng)用程序中應(yīng)用JDOM。在下一篇文章中,我將研究XML Namespace、ResultSetBuilder、XSLT和XPath。你現(xiàn)在可以在otn.oracle.com/oraclemagazine中找到這一系列文章的第二部分。
Jason Hunter (jasonhunter@servlets.com) 是一位顧問(wèn)、Servlets.com的發(fā)行人,Apache軟件基金會(huì)的副總裁。他還是JCP執(zhí)行委員會(huì)的委員