使用 XML:完成 XI 实现 XMLReader 接口

80酷酷网    80kuku.com

  xml专栏作家 Benoit Marchal 继续描述 XI,它是一个将旧文本转换成 XML 的开放源码项目。为了提高效率,XI 现在实现了 SAX XMLReader 接口,这证明了该接口使 XI 链接到 XSLT 处理器变得容易。代码样本演示了这些技术,还可以获得完整的源代码。专栏每个月都报告作者旨在帮助志同道合的 XML 开发人员(尤其是那些使用 Java 技术的开发人员)的开放源码项目。
在上两篇专栏文章中,我一直在讨论 XI(XML Import 的缩写),它是一个将旧文件转换成 XML 的项目(请参阅参考资料)。XI 的动机来自将地址簿发布为 XML 站点的一部分的需求。因为地址簿是用电子邮件客户机的专用格式维护的,所以我需要将文本转换成 XML 的工具。

我利用这一机会尝试了构建到 JDK 1.4 中的新的正则表达式库。正则表达式倾向于一个灵活的转换解决方案:我可以描述如何将旧文档解析成一组正则表达式,而不是对转换例程硬编码。我将对地址簿使用一组规则,但我可以为其它日历或化学分析数据、Web 服务器日志或其它格式编写不同的规则。XI 是一种更常规的工具,您或我都可以在许多项目中使用和重用它。

现在在 XML 中
在上一篇专栏文章“Wrestling with Java NIO”(请参阅参考资料)中,我花了相当长的时间来研究正则表达式库。其结果是我的一些假设完全没有切中要害,但我仍设法使用正则表达式将地址簿解析成元素。

因为我的目标是给出一个常规解决方案,所以我创建了一个小型数据结构来保存这组规则。它使 XML 标记名与正则表达式在本质上相关联。虽然我不得不受限于固定的数据结构以便进行测试,但我组织了代码,这样,从一个文件填充数据结构将是一件简单的事情,这是我目前已经实现的一个特性。

本专栏文章主要是关于清除代码并确保它产生一个有效的 XML 文档。我还设法将现有的算法封装成 XML 解析器。正如您将要看到的,XML 解析器接口证明了它使处理 XSLT 处理器很容易。

编写 XML 文档的最佳方法
完成 XI 的最容易的解决方案就是重访代码,并修改各种打印(print)语句来写 XML 标记。确实,解析文档并将 XML 元素与节点相关联的逻辑早就出现了。例如,当逻辑与正则表达式匹配时,算法打印与之相关联的元素,如:

System.out.print(ruleset.getMatchAt.getQualifiedName());


修改这条语句以产生正确的 XML 并不难:

System.out.print("<"+ruleset.getMatchAt.getQualifiedName()+">");


当然,上面的语句只打印开始标记,所以我需要更多的打印语句用于结束标记和内容,但那做起来并不难。

如果我只对编写 XML 文档感兴趣,那么它或许是我愿意做的,因为它是最不费力的解决方案。要特别注意避免使用尖括号、& 和其它保留字符,但这些都是很琐碎的事情。我还可能要将 XML 文档保存在文件中,而不是打印到控制台 — 但再说一次,这是琐碎的事情。

然而,我并不愿意将 XML 文档写入文件。正如您可以回想起前几篇专栏文章,我不打算直接使用 XI 的输出。经验显示人们常常需要重新组织旧文档。例如,在地址簿中,我必需将 alias 和 note 行组合在一起。我可以将逻辑添加到 XI 中来处理这种情况和其它相似的情况,但我发现将导入过程分成两个步骤很有利:

语法转换
数据结构重组
语法转换获取文本信息并用最简单的 XML 结构封装它。一般情况下,产生的 XML 文档十分接近于原始文件。在大多数情况下,它与用 XML 标记替换定界符一样简单。这是 XI 所做的事情。

第二步是使用转换将这个原始 XML 文档转换成目标词汇表。我发现 XSLT 尤其适合于该目的,因为它是一种功能强大的转换语言。并且因为 XSLT 是一个标准,所以不会缺乏支持工具,如编辑器。

简而言之,我不一定要求 XI 将 XML 文档写在文件中;我宁愿使它最优化,以便与 XSLT 处理器交互。JDK 1.4 携带有 Apache Xalan 的一个版本,它接受来自文件(流)、SAX 事件和 DOM 树的输入。在这三种接口中,我个人最喜欢 SAX。

SAX 很有吸引力,因为它易于编程,并且在处理 XML 文档时有一个相当有效的接口。与文件比较,它将编写的内容保存到临时文件;与 DOM 比较,它需要更少的内存。

对 SAX 接口进行编程
在本文的其余部分,我假设您熟悉 SAX 编程。如果不熟悉,您可能要转到同在 developerWorks 上的“SAX,the Power API”(请参阅参考资料)。

SAX 中两个最重要的接口是 XMLReader 和 ContentHandler。XMLReader 描述如何初始化和启动 XML 解析器,而 ContentHandler 列出当它解析 XML 文档时 XMLReader 发出的事件。

当读取 XML 文档时,您或许已经使用了这两个接口。然而,即使您熟悉它们,但该应用程序要求您从略微不同的角度查看 SAX。在这种情况下,我编写我自己的解析器,而不是成为 SAX 的用户。严格来说,XI 不是 XML 解析器;它不读取 XML 文档。然而,它对提供文本文档的 XML 视图,因此它可以符合 XMLReader 接口。

XI 的 SAX 实现在类 XIReader 中。该类太大,以致于无法在这里完整地复制它。在继续之前,我鼓励您从 developerWorks 的“开放源码”一节(请参阅参考资料)获得一份拷贝。

XIReader 处理两个问题:实现 SAX 接口和实际文本解析以及 XML 文档生成。清单 1 说明了该接口的实现。

清单 1:XIReader 的 SAX 实现

public class XIReader

implements XMLReader, Locator

{

protected ContentHandler contentHandler = null;

public ContentHandler getContentHandler()

{

return contentHandler;

}

public void setContentHandler(ContentHandler value)

throws NullPointerException

{

if(value == null)

throw new NullPointerException("ContentHandler");

else
contentHandler = value;

}

// ...

}


为了支持 XMLReader,XIReader 提供了下列方法来注册和访问各种 SAX 处理程序:ContentHandler、ErrorHandler、DTDHandler 和 EntityResolver。

严格来说,DTDHandler 和 EntityResolver 没有用:旧文本没有 DTD,所以 XIReader 决不会发出与 DTD 相关的事件。

同样,也没有必要使用 EntityResolver;如果您回想起来,解析器不应该将它用于顶层文档实体。该接口只对外部实体(如 DTD)有用!因此,对旧文本文档没有用。尽管如此,SAX 还是授权方法来设置和获取这两个处理程序,并且 XIReader 强迫这样做。

XIReader 还实现对 SAX 功能和特性的有限支持。功能和特性控制解析的各个方面;它们由如 之类的 URL 标识。请注意,URL 仅担当标识符,所以不要设法打开它们。(请不要访问网站 — 没有可访问的网站。)

规范声明 XMLReader 必须支持将 功能设置成 true(支持 false 是可选的),将 设置成 false(true 是可选的)。

第一个功能控制解析器是否对 XML 名称空间(true)解码。XIReader 始终使用名称空间。第二个功能控制是否在属性(true)列表中报告名称空间声明。XIReader 支持两个值。

正如您可看到的,XIReader 提供最小程度的一致性。尽管如此,我还是发现必须支持将 设置成 true(正如规范所需要的,不仅是 false),因为 Apache Xalan 需要将该特性设置成 true 来正确地处理名称空间。

规范定义了其它功能及其 URL,但不要求解析器支持它们。由于这些功能中的大多数都处理有效性和 XML 模式,所以我选择省略它们。

我还定义了一个新特性 来为解析器提供其规则文件。该特性接受指向规则文件的 InputSource 值。

ContentHandler 和解析
在上一篇专栏文章“Wrestling with Java NIO”(请参阅参考资料)中讨论的代码中,大量处理发生在名为 read() 的方法中。我将它重命名为 match() 以提高可读性,并且修改它使它在对输入文档解码时调用 ContentHandler。清单 2 说明了这一操作。如果将该代码与“Wrestling with Java NIO”中的代码相比,您将发现它们的结构十分相似。唯一的重要差别是 print() 语句已经被各种对 ContentHandler 的调用所替换。

清单 2:match() 和 ContentHandler

public void match(Ruleset ruleset,String st,boolean firstMatch)

throws SAXException

{

attributes.clear();

int i = 0;

while(i < ruleset.getMatchCount())

{

if(ruleset.getMatchAt.matches(st))

{

Match match = ruleset.getMatchAt;

if(firstMatch && contentHandler != null)

contentHandler.startElement(match.getNamespaceURI(),

match.getLocalName(),

match.getQualifiedName(),

attributes);

for(int j = 1;j <= match.getGroupCount();j++)

{

QName qname = match.getGroupNameAt(j);

Ruleset nextRuleset = (Ruleset)rulesetsMap.get(qname);

if(nextRuleset != null)

match(nextRuleset,match.getGroupValueAt(j),true);

else
{

Group group = match.getGroupNameAt(j);

if(contentHandler != null)

{

contentHandler.startElement(group.getNamespaceURI(),

group.getLocalName(),

group.getQualifiedName(),

attributes);

String value = match.getGroupValueAt(j);

int begin = 0,

end = 0;

while(begin < value.length())

{

if(value.length() - begin < chars.length)

end = value.length();

else
end = begin + chars.length;

value.getChars(begin,end,chars,0);

contentHandler.characters(chars,0,end - begin);

begin = end;

}

contentHandler.endElement(group.getNamespaceURI(),

group.getLocalName(),

group.getQualifiedName());

}

}

}

String rest = match.rest();

if(rest != null)

match(ruleset,rest,false);

if(firstMatch && contentHandler != null)

contentHandler.endElement(match.getNamespaceURI(),

match.getLocalName(),

match.getQualifiedName());

break;

}

else
i++;

}

if(i < ruleset.getMatchCount()

&& ruleset.getError() != null

&& errorHandler != null)

errorHandler.error(new SAXParseException(ruleset.getError(),

this));

}


XM 是在第一篇使用 XML 专栏文章中介绍的发布项目,如果您记得它,那么您会熟悉发出 ContentHandler 的事件。XM 这样做来修正悬着的超链接。XIReader 构建在同一逻辑上,但它更雄心勃勃。它发出足够多的事件来描述一个完整的文档,而不是为链接发出一个事件。

我承认我最初很想编写完整的 XMLReader 实现。但是,正如本专栏文章所显示的那样,它简直是太容易了……,它恰恰证明了一点,SAX 确实做到了如它的名字所指出的,是用于 XML 的简单 API。

ContentHandler 的使用尤其简单。请考虑一下,您通常有打印开始和结束标记以及内容的方法。这些方法会处理字符转义、缩排和其它与语法相关的问题。ContentHandler 基本上为您定义了这些方法。使用 startElement() 方法打印开始标记,使用 endElement() 方法打印结束标记,使用 characters() 方法打印内容。

读取规则文件
建立了 XMLReader 之后,我想使 XI 能够读取规则文件。我已经不只有可以处理地址簿的 XI 应用程序,所以想打破硬编码的正则表达式的束缚。

我主要保存了在前二篇专栏文章中引入的词汇表。规则文件类似于清单 3。根元素是 rules;它包含一个或多个 ruleset 元素。

每个 ruleset 都包含表示正则表达式的 match 列表。error 元素详细描述当 XI 不能与任何正则表达式匹配时要做些什么。最后,group 元素表示正则表达式中的组。与每个元素相连的是元素名,它是 XI 使用的名称。

清单 3:rules.xml

<?xml version="1.0"?>

<xi:rules version="1.0"

xmlns:xi="http://ananas.org/2002/xi/rules"

defaultPrefix="an"

targetNamespace="http://ananas.org/2002/sample">

<xi:ruleset name="address-book">

<xi:match name="alias"

pattern="^alias (.*).*)$">

<xi:group name="id"/>

<xi:group name="email"/>

</xi:match>

<xi:match name="note"

pattern="^note .*.*)$">

<xi:group name="fields"/>

</xi:match>

<xi:error message="unknown line type"/>

</xi:ruleset>

<xi:ruleset name="fields">

<xi:match name="field"

pattern="[\s]*<([^<]*)>">

<xi:group name="field"/>

</xi:match>

</xi:ruleset>

</xi:rules>


我在原始词汇表和清单 3 中的词汇表之间做了一个更改:现在文档支持一个应用于整个规则文件的全局名称空间。我最初的想法是让用户在规则文件中指定多个名称空间,但它使 XIReader 变得不必要的复杂。

随着我深入研究问题,我认识到全局名称空间满足了所有需求中的 99%。但是,万一您真的需要多个名称空间,那该怎么办呢?您仍可以使用它,因为无论如何文档在 XSLT 中总是后处理的。将新的名称空间添加到样式表中是一件简单的事情。

用 HC 来补救
编写本专栏文章的乐趣之一是当继续该专栏时我可以重用项目。在这个案例中,我使用几个月前介绍的 HC,处理程序编译器(Handler Compiler)来简化解析规则文件。

如果您没有阅读过相应的专栏文章,那么 HC 是获取 XPaths 作注解的 Java 类并将它转换成 SAX ContentHandler 的预编译器。该类中的每个方法都与一个或多个 XPath 匹配。实际上,它省去编写许多冗长乏味的状态管理代码。

清单 4 是规则文件的处理程序。可以在 Javadoc 注释中看到那些 XPath。该处理程序为规则词汇表中的每个元素定义一个方法。当它遍历规则文件时,它用正则表达式填充数据结构。

清单 4:RulesHandler.java

package org.ananas.xi;

import java.util.*;

import org.xml.sax.*;

/**

* xmlns xi

*/

public class RulesHandler

implements org.ananas.hc.HCHandler

{

private String namespaceURI = null;

private String prefix = null;

private List rulesets = null;

private Ruleset getLastRuleset()

{

return (Ruleset)rulesets.get(rulesets.size() - 1);

}

/**

* xpath xi:rules

*/

public void init(Attributes attributes)

{

rulesets = new ArrayList();

namespaceURI = attributes.getValue("targetNamespace");

prefix = attributes.getValue("defaultPrefix");

if(namespaceURI != null)

{

namespaceURI = namespaceURI.trim();

if(namespaceURI.equals(""))

namespaceURI = null;

}

if(prefix != null)

{

prefix = prefix.trim();

if(prefix.equals(""))

prefix = null;

}

}

/**

* xpath xi:rules/xi:ruleset

*/

public void doRuleset(Attributes attributes)

throws SAXException

{

String name = attributes.getValue("name");

if(name != null)

rulesets.add(new Ruleset(namespaceURI,

name,

prefix));

else
throw new SAXException("name attribute required for xi:ruleset");

}

/**

* xpath xi:rules/xi:ruleset/xi:match

*/

public void doMatch(Attributes attributes)

throws SAXException

{

String name = attributes.getValue("name"),

pattern = attributes.getValue("pattern");

if(name != null && pattern != null)

{

Ruleset ruleset = getLastRuleset();

ruleset.addMatch(new Match(namespaceURI,

name,

prefix,

pattern));

}

else
throw new SAXException("name and pattern attributes" +

"required for xi:match");

}

/**

* xpath xi:rules/xi:ruleset/xi:error

*/

public void doError(Attributes attributes)

throws SAXException

{

String message = attributes.getValue("message");

if(message != null)

{

Ruleset ruleset = getLastRuleset();

if(ruleset.getError() == null)

ruleset.setError(message);

else
throw new SAXException("no more than one error per xi:ruleset");

}

else
throw new SAXException("message attribute required for xi:error");

}

/**

* xpath xi:rules/xi:ruleset/xi:match/xi:group

*/

public void doGroup(Attributes attributes)

throws SAXException

{

String name = attributes.getValue("name");

if(name != null)

{

Ruleset ruleset = getLastRuleset();

Match match = ruleset.getLastMatch();

match.addGroup(new Group(namespaceURI,

name,

prefix));

}

else
throw new SAXException("name attribute required for xi:group");

}

public Ruleset[] getRulesets()

{

Ruleset[] array = new Ruleset[rulesets.size()];

return (Ruleset[])rulesets.toArray(array);

}

public String getNamespaceURI()

{

return namespaceURI;

}

public String getPrefix()

{

return prefix;

}

}


直到下一次
处理 XI 的工作接近完成了。现在,您有了一个运行的处理器,如清单 5 说明的那样,使它与 XSLT 处理器交互是一件简单的事情。在下一篇专栏文章中,我将围绕现有核心介绍简单的用户界面,以使 XI 更有用。

清单 5:样本 Main 方法

public static void main(String[] params)

throws TransformerException, TransformerConfigurationException,

SAXException, IOException

{

InputSource inputSource = new InputSource(new FileInputStream(params[0]));

inputSource.setSystemId(params[0]);

XMLReader xmlReader =

XMLReaderFactory.createXMLReader("org.ananas.xi.XIReader");

xmlReader.setProperty(XIReader.RULESETS_URI,new InputSource("rules.xml"));

TransformerFactory factory = TransformerFactory.newInstance();

Transformer transformer = factory.newTransformer();

transformer.transform(new SAXSource(xmlReader,inputSource),new

StreamResult("result.xml"));

}


分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: