IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML  >

使用 XML: 为 SAX ContentHandler 构建编译器

启动一个新项目 HC

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Benoit Marchal (bmarchal@pineapplesoft.com), 顾问, Pineapplesoft

2001 年 11 月 01 日

本月,Beno顃 Marchal 启动了第二个<i>“使用 XML”</i> 项目。这个名为 HC(处理程序编译器 ― Handler Compiler 的缩写)的新项目通过自动为 XPaths 列表生成 SAX <code>ContentHandler</code> 来承担基于事件的 XML 解析中的一些繁重任务。本文是这个专栏的一部分,它描述了 Java 项目的要求,并对包括 <code>ContentHandler</code> 和转换图在内的整体设计进行了分析。

上个月,我暂停了 XM 项目(一个基于 XSLT 的 Web 发布管理应用程序),同时我在其后几个月中收集有关 XM 的反馈意见。已经有一些读者写了反馈意见,与大家分享使用 XM 的经验,报告了一些错误并提出改进建议。尤其感谢您们花时间与大家分享想法。

那么,就开始新项目吧。在这个 “使用 XML” 专栏的第二个项目中,我想自动地创建 SAX ContentHandler 。如果您对 SAX 不熟悉,请看一下 SAX,强大的 API,它是摘自 XML by Example,2nd Edition

人们将 SAX 设计成为面向语法的 API。通过 SAX 事件,应用程序侦听文档流,并且按照需要生成相应的响应。SAX 非常适合于转换,另外也适合于操作 XML 文档。它既简单又有效。

然而,SAX 的设计只考虑了功能的强大性,却没有考虑程序员使用起来是否方便,而这个问题对于编写事件处理程序,却显得尤为明显。SAX 几乎不做预处理,因而,程序员不得不管理很多低级别问题。尤其是 SAX 事件处理程序特别重要的一部分是致力于跟踪解析器已在文档中的执行程度。状态跟踪代码的重复性很高,而且很容易出错,一般而言,编写和维护该部分是很繁琐的。

我在本专栏中介绍的处理程序编译器 HC 应该可以简化内容跟踪代码的编写。具体地说,HC 将自动编写一个 SAX ContentHandler

高级别的规范

为更好地了解 HC 的所有内容,请看 清单 1 。清单 1 所介绍的 ContentHandler 只是计算 simpara 元素中 ulink 元素出现的次数。它忽略在文档其它部分出现的 ulink ,例如, articleinfo 元素中的 ulink 元素就被忽略了。

很难想象有比这更繁琐的工作:当您发现一个特殊元素时就将计数器加 1。尽管 SAXCountHandler 只有 39 行,但这 39 行中,仅有 12 行直接与计数器有关。大部分代码都在跟踪解析器的状态。


清单 1. SAXCountHandler.java
import java.util.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
public class SAXCountHandler
   extends DefaultHandler
{
   private int count;
   private Stack stack;
   public void startDocument()
   {
      count = 0;
      stack = new Stack();
   }
   public void startElement(String uri,
                            String lName,
                            String qName,
                            Attributes atts)
   {
      if(!uri.equals("http://www.psol.com/2001/docbook"))
         return;
      if(lName.equals("ulink") &&
         "simpara".equals(stack.peek()))
         count++;
      stack.push(lName);
   }
   public void endElement(String uri,
                          String lName,
                          String qName)
   {
      if(!uri.equals("http://www.psol.com/2001/docbook"))
         return;
      stack.pop();
   }
   public int getCount()
   {
      return count;
   }
}

清单 1清单 2 比较,这就说明了我所希望编写的内容。 HCCountHandler 类的大小是 SAXCounterHandler 的一半,而且这段代码的绝大部分都是处理这个计数器。事实上,在 HCCountHandler 中没有代码去跟踪状态。相反,它用特殊的 @xpath 来指出应该在何时调用哪个方法。

HC 编译器将对清单 2 进行预处理,并使用 @xpath 注释自动生成状态跟踪代码。这里最重要的词是 自动:HC 不会废除状态跟踪代码(这也不可能),但它使程序员省去了许多编写状态跟踪代码的功夫。


清单 2. HCCountHandler.java
/**
 * @xmlns http://www.psol.com/2001/docbook
 */
public class HCCountHandler
   implements org.ananas.hc.HCHandler
{
   private int count;
   /**
    * @xpath /
    */
   public void init()
   {
      count = 0;
   }
   /**
    * @xpath simpara/ulink
    */
   public void increment()
   {
      count++;
   }
   public int getCount()
   {
      return count;
   }
}





回页首


分析

既然知道了目标,那么让我们看一下如何去实现它。

HC 是一个编译器;它接受带有特殊注释的 Java 源代码作为输入,并生成将类转变成有效 SAX ContentHandler 所需要的全部内容。这里要分析两方面:HC 需要生成什么以及它如何生成?下一节探讨第一个问题的答案 ― 代码。随后的几节则讨论第二个问题的答案 ― 编译器本身。

为简单起见,我决定至少在 HC 的第一个发行版中将 HC 的范围限制为不带条件的 XPath。例如,在发行版 1 中,下列内容是可以接受的:

/
/article/articleinfo/title
simpara/ulink

但下面内容在版本 1 中还不能被接受:

simpara[ulink/@href='index.xml']

我充分理解到条件有许多用处,但若没有这些条件,则带来的简便性却使我可以在识别出 XPath 的即刻就发出一个事件。如果要考虑条件的话,则必须存储这个事件,直到满足条件为止。如同以往一样, “使用 XML”专栏总是本着这样的思想:在专栏进行过程中,开发代码不断深入,以便使您清楚地了解整个项目。我将在 HC 今后的版本中取消对条件的约束。

生成的代码

图 1 显示了正在生成的代码的类模型。HC 提出一个实现 ContentHandler 接口的标准 XPathHandler 类。 XPathHandler 的任务之一是为我们跟踪那些讨厌的状态。在清单 2 中的示例里, 应用处理程序 HCCountHandler 将调用 XPathHandler


图 1.生成代码所用的类模型
生成的代码

该体系结构是标准的代理模式。 XPathHandler 类充当由 SAX 定义的低级别一般事件和由我们定义的高级别特定于应用程序事件之间的代理。在该过程中,HC 使程序员摆脱了状态管理的繁琐工作。

最后,我希望能够编写如下的代码:

HCCountHandler counter = new HCCountHandler();
XPathHandler handler = new XPathHandler(counter);
reader.setContentHandler(handler);
reader.parse(uri);

显然,为了执行它这种神奇功能, XPathHandler 需要应用处理程序的描述。编译器将用 XPathHandler 所需的信息生成一个额外的类,即 table类。table 类包含几个表和一些方法,以执行特定于应用程序的调用。

table 类的名称无关紧要,因为它是自动生成的,而且实际上程序员看不到它。编译器只需附加一个足够长和足够奇特的后缀,就可以避免冲突: __org_ananas_hc_tables

编译器前端

编译器负责自动创建 table 类。编译器的设计标准是将其分成两个模块:前端和后端,前端读取输入文件并对该文件进行解码,后端生成代码,或者在这种情况下生成 table 类。

HC 也没有偏离这个标准,但是它使用了一个简化编写前端的技巧。回忆一下清单 2 中以 Javadoc 注释形式编写的 XPaths。我发现这样做之所以方便有两个原因。首先,它是一个更方便的常用语法。其次,它通过 doclet 机制向我提供了自由的前端。

doclet 是 JDK 1.2 引入的一种机制,它扩展了 Javadoc。简而言之,doclet 是一个 Java 类,Javadoc 将解析 Java 类的结果传递给 doclet。最初设计的 doclet 是用来更多地控制文档的显示。例如,您可以编写一个 doclet 来生成 PDF 文档,而不产生标准 HTML 输出。

然而,Javadoc 返回很多信息,这些信息包含类名、类所在的包、方法列表和它们的参数,当然还有一些大名鼎鼎的注释,因此使用 Javadoc 作为前端不仅有可能而且很简单。唯一遗漏的就是方法的实现…… 但 HC 不需要这些。

com.sun.javadoc 包引入了 doclet。主要的类是包含解析树的 RootDoc 。这些类本身是以 ClassDoc 的形式表示,方法是以 MethodDoc 的形式表示,而 Javadoc 注释则是 Tag 类的实例。所以 @xpath 注释最终将作为附加在 MethodDoc 对象上的 Tag 对象返回。

请注意,因为 Javadoc 不解析 XPath,所以我不得不在前端添加一个 XPath 解析器,但是与解析 Java 类相比,解析 XPath 要容易一些。

编译器后端

现在进入本项目最有趣的部分:后端。后端收集路径信息并创建 table 类。尽管此时我还没有得出这种算法的所有详细信息,但我打算使用 转换图,譬如图 2 中的转换图。转换图实质是控制如何识别 XPath 的流程图。这些圈表示 状态。状态由一些称为 的箭头连接,这些边指出了哪一个输入元素导致了从一种状态移动到下一个状态。


图 2. simpara/ulink 的转换图
转换图

所以转换图指定了如何识别给定的 XPath。状态保留跟踪解析器读取文档时处在位置所需的必要信息;边记录了从一种状态到下一种状态的进展。

带有粗边框的状态(表示 XPath 的结束)是 接受状态。一般而言,当过程进行到接受状态时,希望对给定的 XPath 调用应用处理程序。显然在实际情况下,您一定会有比这复杂的转换图,这些转换图有好几个接受状态,您想要识别的每一个 XPath 都有一个接受状态。

转换图是用于词法分析器和解析器的公共实现。转换图对于搜索文本也是有效的。因为有可能将该图有效地表示为一组表,所以它们特别吸引人。

确定性有限自动机

处理转换图的最常见算法是在 确定性有限自动机(DFA)中转换它们。几年前,我在处理正则表达式时,第一次遇到了 DFA,但是我已经至少有三年没有用过它,所以我花了一些时间来研究它。主要的是,我重读了“龙书”的第 3 章和第 4 章(龙书是有关编译器构造的经典书籍)(请参阅 参考资料)。

在数学领域,转换图也是一组状态。这些状态中有一个是 开始状态,还有一个或多个接受状态。转换图还包含 输入符号(在本例中为 XML 标记)和控制输入符号如何修改状态的 转换函数

可将该模型有效地表示为类似于表 1 的 转换表 。该表提供了实现转换函数所需的所有数据。例如,它规定当遇到 simpara 标记时,状态 1 转换到状态 2。这样的表用来实现 非确定性有限自动机(NFA)。NFA 仅仅是一个用于表示实现转换图的状态机的华丽词语。

表 1. simpara/ulink 的转换表

输入符号
状态 simpara ulink

但是为了提高效率,最好使用确定性有限自动机。DFA 是 NFA 的一个特例:它在遇到空字符串时不转换,而且保证对于每个符号,最多只有一条边离开任何给定状态(换句话说,从每个状态都可以确定性地计算出转换)。正如您将看到的,有可能自动地将 NFA 转换成 DFA。

转换表中的状态所实现的功能等价于 SAXCountHandler 中 stack。有了转换表,实现转换函数就没有价值了。一旦准备好转换函数,就可以轻易地实现 NFA。请记住,NFA 是一个实现转换图的状态机。总而言之,如果可以编译转换表的话,则可以轻易地处理 XPath。

幸运的是,在对任意一个转换图构造转换表方面,有一些成文的算法,这样,DFA 就可以有效地解决这个问题。

转换表的唯一缺点就是在实际的应用程序中,状态的数量增加得非常快。 手工编写转换表工作量很大。幸运的是,您不是手工编写它,而是编译它。

该代码被分成三个包:

  • org.ananas.hc 是运行时。它包含了那些必须始终与应用程序包含在一起的类。该包应该尽可能的小。
  • org.ananas.hc.compiler 是一个编译器。它创建 XPathHandler 所需的 table 类。该包还包含 doclet 和 XPath 解析器。
  • 应用程序本身的包,它由 table 类创建。




回页首


下一步

我在本月的 “使用 XML”专栏中专门分析了 HC 系统,并研究了算法和设计了主要的类和包。在我下个月开始编写编译器的代码时,才开始真正有趣的内容。一如既往,一旦该代码可用就会将其放在 CVS 资源库中。



参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 请参加关于本文的 论坛

  • 所谓的“ 龙书”( Compilers: Principles, Techniques and Tools,由 A. Aho, R. Sethi 和 J. Ullman 合著)是一本有关编译器设计和构造方面具有权威性的文档。

  • SAX,强大的 API摘自 XML by Example, 2nd Edition,这本书介绍了 SAX 解析。

  • JAXP是标准的 Java API for XML 处理。它包含三个组件:SAX、DOM 和 TrAX。

  • 来自读者 Robert Berlinski 的 SIA 解析器是另一种自动化引擎。它可以采用不同的方法,因为它不使用编译器。

  • 各种 IBM 的工具箱给 XML 开发者提供了帮助。请浏览 工具箱摘要,其中可能得到一些您所需要的。

  • XML Master(别名 Xmas)采用了完全不同的方法来简化解析:它为 XML 文档中的特定元素生成 JavaBean。


关于作者

Author photo

Benoît Marchal 是住在比利时那慕尔的顾问和作家。 他是 XML by Example Applied XML Solutions XML and the Enterprise 的作者。同时是 Gamelan 的专栏作家。 www.marchal.com 上有其最新项目的详细信息。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款