本章主要讨论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 ,、startElement 、 endElement 和characters 。
实现该接口的最简单的方法是扩展DefaultHandler 类,该类定义在org.xml.sax.helpers 包中。该类为所有的ContentHandler 事件提供了不完成任何实际工作的方法。输入下面突出显示的代码,扩展该类: public class Echo extends DefaultHandler { ... }
注意:DefaultHandler 也为其他定义在DTDHandler 、 EntityResolver 和ErrorHandler 接口中的主要事件定义不完成任何工作的方法。下面,你会进一步了解这些方法。
接口需要所有的这些方法来抛出SAXException 。这里抛出的异常会送给解析器,然后解析器再将其发送给调用解析器的代码。在目前的程序中,这意味着它在main方法底部的Throwable 异常处理程序 中结束。
遇到起始标签或结束标签时,将标签的名字作为String 传递给startElement 或endElement 方法。遇到起始标签时,也通过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 方法抛出SAXException s 而不是IOException s,它可能在编写的时候出现。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解析器应用程序。下一步是编译并运行它。
注意:为了确保精确度,字符处理程序必须扫描缓冲区看看有没有字符('&'); 和 左尖括号('<'),并且使用字符串 "& " or "< "替换它们。在替换和插入文本中的实体引用讨论中,会获得该类处理的更多信息。
编译并运行程序
在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 方法:setDocumentLocator 和processingInstruction 。 本节主要介绍如何实现这两个事件处理程序。
识别文档的位置
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" 。所以有getSystemId 和getPublicId 。
在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或模式的情况下很难产生警告, 所以你现在还看不到任何警告。但是如果出现了警告,你也能处理。 |