[转载]使用 XML: 为 SAX ContentHandler 构建编译器

使用 XML: 为 SAX ContentHandler 构建编译器
用于 SAX 解析且基于 Java 的“处理程序编译器(Handler Compiler,简称 HC)”项目即将推出它的 alpha 发行版。本月,通过给出一个使用该编译器来识别 XPath 的具体示例,我们的专栏作家描述了他是如何实现 DFA 构造算法的。 在“使 用 XML”这个专栏中,作者 Benoit Marchal 每月都会针对 XML 开发者讨论他的开放源码项目的进展,其范围包括从设计决策到编码挑战在内的多方面内容。当前这个名为 HC(处理程序编译器 ― Handler Compiler 的缩写)的项目通过自动为 XPath 列表生成 SAX ContentHandler 来承担基于事件的 XML 解析中的一些繁重任务。

在上两篇专栏文章之前,我启动了 HC(处理程序编译器)项目作为本专栏的一个新项目。HC 的目标是编译一个 代理内容处理程序,该代理内容处理程序使 XPath 与 SAX 解析中的特定方法( 应用处理程序 ― application handler)相匹配。

我发现自己在进行 SAX 编程时,在一些低级的、重复性的工作(如,状态跟踪)上花费了过多时间。为了尝试从这些技术细节中解脱出来,我启动了 HC 这个项目。

如果您还没有阅读过上两篇专栏文章,则我鼓励您浏览一下那两篇文章(请参阅右侧“相关内容”中的链接),因为这个月我要实现那两篇文章中介绍过的算法。

讲得更具体些,在上篇专栏文章中,我回顾了编译所谓“确定性有限自动机(Deterministic Finite Automaton,简称 DFA)”的一些算法。DFA 是一个深受大家喜爱的算法,用来构造一个状态机来识别模式。在 HC 中,这些模式就是 XPath。

消息和显示
这篇专栏文章中所涉及的大多数开发工作都是在实现上月所介绍的 DFA 构造算法。然而,在真正实现算法之前,我首先必须实现一些用于消息显示的实用类(utility class)。如果您定期阅读本专栏,则您应该很熟悉这些类中的大多数:

  • 从 Java 刚出现起,我就一直很怀念如标准 Java 库中的 NotImplementedException 这类库函数。这是一个告诉人们出现意外情况的异常。我发现,在跟踪错误时,用特定的错误码(而不是一般的异常或 ― 甚至更糟 ― 不报告错误)来报告这些异常节省了我数小时的时间。我在 org.ananas.util 包中为 XM 引入了类似的类。这个版本的类在 org.ananas.hc 包中,这表明它属于 HC 运行时。
  • Messenger 非常类似于 XM 的 messenger 类。通过 Messenger 接口的实现,编译器与用户进行交互。这个接口有一些方法来报告错误和显示信息消息。由于这个编译器是以批处理方式来工作,因此它没有方法来提示用户输入数据。请注意,这个类是该编译器独有的(所以,该类位于 org.ananas.hc.compiler 包中)。HC 运行时(包括代理)抛出 SAXException 或 SAXException 的后代以报告错误。
  • DefaultMessenger 是 Messenger 的缺省实现,它将消息输出到控制台。为了在该编译器上提供一个 GUI 前端,仅需要提供一个 Messenger 的替代实现,它在窗口中显示消息。
  • MessageStore 提供国际化的便利条件。它实际是将 ResourceBundle 与 MessageFormat 中大多数有用的方法组合在一起。

虽然,在整个 HC 操作中,这些类是很重要的(设想一下,如果一个编译器不能报告错误,那会是怎样?),但它们与编译 XPath 没有直接联系,并且这些类大多数都简单明了,无须额外解释。因此,在本专栏文章中,我将不再讲述这些类。相反,我希望您下载这些类,自己来看一下代码(请 参阅 参考资料)。

XPath 解析
当构造 DFA 时,两个相关元素发挥了作用。从上一篇专栏文章所讨论的内容中,您可以回忆起,构造 DFA 的算法需要一棵解析树作为输入。对于 HC,该解析树表示一个 XPath。

XPathNode
在上一篇专栏文章中,我为解析树中的节点定义了一个 HCNode 类。当实现该算法时,我意识到遗忘了两件重要的事情:

  • 在上一篇专栏文章中所介绍的 HCNode 中没有一些字段来存储在成功识别 XPath 时该做些什么。我添加了一个通用对象 data 来保存这样的信息。我还引入了一个 priority 整数来表示 XPath 的相对优先级。
  • 没有实现上一篇专栏文章中解释过的 followpos() 函数。现在,我已经添加它了。

我选择通用对象来存储匹配 XPath 时的信息,是因为不希望将解析器局限于给定的应用程序。当匹配 XPath 时,DFA 编译器是不负责确定要做什么以及如何分配优先级这些事。

至于 followpos() ,我在类构造器中添加了一个取值(方法)和新代码:

case PARENT_OF:

// compute lastpos() and firstpos()

Iterator iterator = left.lastpos().iterator();

while(iterator.hasNext())

{

XPathNode n = (XPathNode)iterator.next();

n.followpos.addAll(right.firstpos());

}

break;

如在上一篇专栏文章中所编写的, HCNode 封装了 QName 。后者表示一个 XML 元素,而 HCNode 表示 XPath 中的一个元素(所以它可能是一个 XML 元素或是一个表示“……之子”的关系)。

在处理 QName 时,我发现它与 HCNode 之间的区别不是很明显。逻辑上,应由 QName (而不是其它类)来表示 XPath 中的 XML 元素。

为可读性起见,我选择用继承 QName 的类来代替 HCNode 。为了更好地反映这种用法,我将这个类的名称改为 XPathNode 。在其它方面, XPathNode 与以前所介绍的 HCNode 完全一样。更明确地讲,它有 firstpos() 、 lastpos() 和 followpos() 方法。

遗憾的是,这造成了一个问题: XPathNode 从 QName 继承了 equals() 和 hashCode() 。这最终导致难以跟踪 followpos() 中的错误。

这个问题在于, QName 表示输入词汇表中的符号。如果两个符号有相同的名称空间 URI 和本地名称,则这两个符号是完全一样的。换句话说, simpara 等于 simpara 。

但是,对于 XPathNode 却不是这样!它们表示 XPath 中的某一确定元素,以便 simpara/ulink 中的 simpara 不同于 simpara/emphasis 中的 simpara 。

讲得更具体些,它们的 followpos() 应该是不同的:在第一种情况中是 ulink ,第二种情况则是 emphasis 。

我必须为 XPathNode 重新编写 equals() ,以便在两个不同的节点出现在不同的 XPath 时,区别对待这两个节点。出于调试目的,我额外地实现了一个用于比较的 isSynonymousWith() 方法。这个新方法用来比较两个 XPathNode 。

通过递归地比较当前节点下的树,该方法可以执行更深一步的比较,或者只是执行停留在当前节点的这种快速比较。我只是打算在调试时使用这个方法,它可以让我用所期望的结果与解析的结果作比较。

在使用 XPath 解析器时,我还确定:应该将 END_MARK ,这种标记 XPath 结束的特殊类型的 XPathNode / HCNode 更好地表示为 QName ,所以我将该常量从 XPathNode 移到 QName 。

在我们还在讨论 XPathNode / HCNode 时,我发现 java.util.Set 有一个 addAll() 方法,它与我所编写的 merge() 是一样的。现在,我使用 addAll() 而不使用 merge() 。

最后一个(但不是最不重要的),我已经为这些类改写了 JUnit 测试。可以从 CVS 资源库(请参阅 参考资料 )下载 XPathNode 及其测试。

XPathParser
XPathParser 负责实际的 XPath 解析,如 清单 1所示。如在以前专栏文章中所提到的,这个类只识别 XPath 的子集,这与我早先的决定 ― 不支持 XPath 中的条件 ― 相一致。

这个类构造器最多有三个参数:

  • NamespaceSupport 是一个由 SAX 定义的、处理名称空间的类。它可以将 XML 限定名称( db:simpara )拆成名称空间 URI 和本地名称。
  • 本质上, MessageStore 是一个资源束,它带有用来报告解析错误的消息。
  • 解析器使用 Messenger 来向最终用户报告错误。

大多数解析在 xpath() 方法中处理。该方法是一种普通的解析。它使用标准的 StringTokenizer 将字符串拆成基本标记。接着,该方法对这些标记进行循环以查找 XML 元素、属性和表示“……之父”关系的分隔符( / )。

在识别标记时,该方法创建 XPathNode ,它带有相应的类型: ELEMENT 、 ATTRIBUTE 和 PARENT-OF 。必须注意绝对 XPath 指示符。

实际上, XPathParser 的用户使用 axpath() 方法的次数要比使用 xpath() 多。 axpath() 方法返回称之为扩张的解析树 ― 该解析树额外地带有一个表示 END_MARK 的节点。这个额外的节点保存关于匹配 XPath 时要做什么的信息。如上面所讨论的,出于那一目的,它使用 data 和 priority 字段。

我还编写了用于该解析器的 JUnit 测试程序。

DFA 构造
DFAFactory 类从该解析树编译 DFA。它在 createDFA() 方法中实现了上月所讨论的算法。目前,该方法接受解析树(作为一个 XPathNode 对象)作为参数,并返回 DFATable 的实例。

DFATable
DFATable 是一个临时类,主要是为了测试而引入它。最终,正如在第一篇 HC 的专栏文章中所解释的,它会被 table类所替代。这个类提供三个有用的方法:

  • 一旦命中给定的 XML 元素, move() 返回 DFA 转换的状态。对于 XML 元素,它的参数是 QName 和当前状态。
  • 如果当前状态是接受状态,则 isAcceptingState() 返回真。接受状态是指,已识别了 XPath 时的状态。在解析树中,它对应于命中 END_MARK 。
  • getAssociatedData() 返回与 XPath 相关的数据(或者为空,如果当前状态不为接受状态)。通常,当已经识别了某个 XPath 时,该数据会告诉我们调用哪些方法。

清单 2 是 DFATable 。如您所见,这个类很简单。它有三个分别用于 DFA 识别的 QName 列表的数据字段:( symbols )、转换表( dtran )和接受状态( astate )。

DFAFactory
清单 3 中的 DFAFactory 是 HC 编译器中的核心类,因为它负责编译转换表。 createDFA() 是以前专栏文章中所讨论过的 DFA 构造算法的直接实现。事实上,这个算法出现在该代码的注释中。

我认为,阅读该代码时,最困难的方面是认识到状态是由一组 XPathNode 表示的。请记住在第一篇 HC 专栏文章中对状态和堆栈的讨论:状态表示特定的堆栈配置(即,出现在堆栈中的一组 XML 元素和属性)。

在该算法中,一组 XPathNode 表示一个堆栈配置。事实上,如果跟踪该算法的过程,就会认识到它构建一个所有可能的堆栈配置以及在这些配置间的转换的列表。

该算法中的顶层循环对可能出现在 XML 文档中的 XML 元素和属性的列表进行迭代。为了计算这个列表, DFAFactory 遍历该解析树,并通过 uniqueSymbols() 方法来编译一个唯一的 QName 列表。

通过检查状态是否与 END_MARK 节点相一致, createDFA() 还构建了一个接受状态的列表。

e_closure() 是一个助手方法,它计算 followpos() 中的节点列表。

测试编译器
虽然 HC 还不十分完整,但它的后端已经基本编写完成。现在只差代理和前端。

为了测试 DFA 构造,我必须模拟代理和前端。 清单 4 是 DFAHandler (一个模拟代理的类)。但它不调用应用处理程序;它只将其自身限于打印已识别的 XPath。

该处理程序获取 DFATable 的实例,并使用 move() 函数来转换到新状态(在给定 XML 元素的情况下)。

进一步设想,您对 db:sect1/db:simpara/db:ulink 和 db:sect1/db:simpara XPath 感兴趣。下面摘录的这段代码显示了如何创建识别 db:sect1/db:simpara/db:ulink 和 db:sect1/db:simpara XPath 的 DFA:

NamespaceSupport namespace =

new NamespaceSupport();

namespace.declarePrefix("db",

"http://www.ananas.org/2001/docbook");

MessageStore message =

MessageStore.getMessageStore(

"org.ananas.hc.compiler.Message");

XPathParser parser =

new XPathParser(namespace,message);

String xpath1 = "db:sect1/db:simpara/db:ulink",

xpath2 = "db:sect1/db:simpara";

XPathNode first = parser.axpath(xpath1,1,xpath1),

second = parser.axpath(xpath2,2,xpath2),

root = new XPathNode(XPathNode.OR_XPATH,

first,second);

DFAFactory factory = new DFAFactory();

DFATable table = factory.createDFA(root);

XMLReader xparser =

XMLReaderFactory.createXMLReader(

"org.apache.xerces.parsers.SAXParser");

xparser.setContentHandler(new DFAHandler(table));

xparser.parse("file:doc.xml");

对于 HC,下一步是什么
DFAHandler 展示了,HC 不久将如何工作。无可否认,前面的示例针对两个 XPath 进行了硬编码,但那只是因为 HC 仍然需要一个前端(在下一篇专栏文章中会涉及到)。

然而,您可能已经意识到了使用编译器的强大功能:DFA 构造算法来负责所有状态管理。

有关 XM 有趣的反馈意见
对于 XM 的状态,这里有一个快速更新。您可以回想起:XM(XSLT Make 的缩写)是“ 使用 XML”专栏中介绍的第一个项目。对于用 XML 和 XSLT 来发布网站,XM 是一个能承担此任务的解决方案。我暂停了 XM 开发是为了收集用户的反馈意见。

迄今为止,我已经在 ananas 讨论邮件列表上收到了一些反馈意见,但我还是很想收到更多来自您那里的意见,因此希望大家能自由踊跃地发表意见。在上月,我还用 XM 构建了两个新网站。第一个网站是用 XML 部分重写了 Pineapplesoft(我的咨询公司)公司的网站。第二个网站是我的一个简短的咨询任务,旨在帮助一家组织实现 XML 和 XSLT。

邮件列表和其它一些经验使我发现了几个错误,而且我已经纠正了其中的大部分。邮件列表和经验还让我更深入了解了第三方是如何部署 XM 的。这些经验还促使我要增加两个新特性:

  • 现在,XM 可以选择忽略文档的外部 DTD。对于 SoftQuad XMetaL(以及可能其它的 XML 编辑器),这证明是很便利的。我发现,XMetaL 插入相对于其目录之一的路径,有一个令人遗憾的副作用:会破坏其它 XML 应用程序(包括作出这些纠正之前的 XM)。
  • 现在,XM 完全支持 xsl:output 元素。所以,可以使用 XM 来对 XML 文档进行预处理,例如,进行大量 XML 转换。

我将在以后的专栏文章中更详细地讨论这些变化。同时,我向 CVS 资源库提交了该新代码。我鼓励您下载它,测试它,并向 ananas 讨论邮件列表(请参阅参考资料)报告您的发现。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-132236/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/374079/viewspace-132236/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值