ITPub博客

首页 > Linux操作系统 > Linux操作系统 > [转载]使用 XML: 编译代理

[转载]使用 XML: 编译代理

原创 Linux操作系统 作者:dinner1007 时间:2019-04-23 13:27:05 0 删除 编辑
使用 XML: 编译代理
在本专栏文章中,Benoit 提供了“处理程序编译器(Handler Compiler,HC)”的前端,并介绍了 DFA 遇到的意外问题。有一个稳定但并非最佳的解决方案使发行用于进一步测试的 HC 的第一个版本成为可能。

本专栏文章是关于“处理程序编译器(HC)”开发的当前回合的最后一篇,它交付了 HC 的第一个工作版本。正如我对 XM(本专栏的第一个项目)所做的那样,我现在打算在接下来的几个月里,在对 HC 进行领域测试的同时再转移到另一个项目。

我希望利用这次机会从项目中获得更多经验,并描绘出未来开发的需求列表。当然,同时,我还鼓励您下载 HC,在您的环境中测试它,并在 ananas 讨论邮件列表共享您的想法。我将在 CVS 服务器上公布错误修正和更新(请参阅 参考资料)。

我们所处的位置
HC 因我编写 SAX 代码时的经验而有所发展。在我欣赏 SAX 解析器的功能和灵活性的同时,我也发现它们需要进行大量冗长乏味的编码以跟踪解析器在文档中的位置。HC 会自动从 XPath 生成状态跟踪代码。

HC 分为两个组件。第一个是编译器,它接受 应用处理程序,并创建一个 表类。应用处理程序是一个实现 HCHandler 接口的 Java 类。专门的 Javadoc 注释表明在解析器匹配 XPath 时要调用的方法。该编译器仅用于开发。

第二个组件是运行时。其最重要的类是 XPathHandler 。 XPathHandler 充当代理,以将调用中的 SAX 事件(起始元素、结束元素等事件)转换成应用处理程序。运行时与应用程序一起交付。

图 1 是 HC 运行时的类模型。其中,应用处理程序是 HCCountHandler。

图 1. HC 运行时的类模型
生成的代码

在上一篇专栏文章中,我们编写了在所谓的“确定性有限自动机(Deterministic Finite Automaton 或简称 DFA)”中编译一组 XPath 的逻辑。不必重复那篇专栏文章所讨论的内容,DFA 是可以高效编译 XPath 的结构。

本专栏文章的其余内容是将 DFA 构造算法与 Javadoc 通过接口连接,以从应用处理程序源代码中取出 XPath。在本专栏文章中,我们还需要编写表类。

遗憾的是,我原指望上述方法成为通往项目结束的平坦道路,但在发现 DFA 的问题时,它却变成了一个更棘手的编码会话。这将在后面有更详细讨论。

Javadoc Doclet
要将 HC 平滑地集成到 Java 类中,我求助于一个老朋友:Javadoc 注释。新的 @xpath Javadoc 标记表明方法应该匹配给定的 XPath,如:

/**
* @xpath para
*/
public void startPara()
{
writer.print("

");
}

要注意:这里我们有两种不同标记。Javadoc 标记出现在 Java 代码中,形式是 @name value 。XML 标记出现在 XML 代码,形式是 。不幸的是,Javadoc 和 XML 采用相同的词汇,所以小心不要把它们混淆了。

我喜欢这个解决方案,因为它没有强迫我从 Java 编辑器切换到另一个工具或者学习一种新语言。而且,自 JDK 1.2 开始,Javadoc 开始支持 Doclet 扩展。Doclet 让您将任何代码插入 Javadoc 解析器。

最初引入 Doclet 是为了让您更改 Javadoc 文档的格式。Javadoc 解析器读取文件,编译有关类、方法和包的信息,然后将信息传递给 Doclet。缺省 Doclet 编写类的 HTML 文档。Sun 也交付了用于 MIF(Framemaker)、PDF 和 RTF 的 Doclet。

Doclet 有许多其它应用程序。因为它们有权访问整个解析树(除去实际方法主体),所以 Doclet 提供了一个简便机制来编译有关代码的实用程序类或报告。例如,Sun 有一个 Doclet,它检查注释的质量和一致性。

Doclet API
Doclet 是按照 main() 方法形成的。Doclet 有静态 start() 方法,并将解析树作为参数。

Doclet API 定义许多类用于存储解析树(注意,这是 Java 解析树,而不是 XML 树)。用于 HC 的最重要的类有: RootDoc 、 ClassDoc 和 MethodDoc ;它们分别返回有关解析树、类和方法的信息。

HC 编译器是 CompilerDoclet 。它收集名称空间声明(从 @xmlns 标记获得)和与方法连接的 XPath(从 @xpath 标记获得),并使用 HCTablesGenerator (将作简略介绍)来编写表类。

CompilerDoclet 的完整清单可在线获得(请参阅 参考资料)。清单 1 是 start() 方法。它抽取命令行参数,然后处理解析树。注意,使用内部类 DocletMessenger 正确报告 HC 错误。

清单 1:CompilerDoclet.start()
public static boolean start(RootDoc root)
throws Exception
{
try
{
String[][] options = root.options();
File destdir = new File(".");
for(int i = 0;i < options.length;i++)
if(options[i][0].equals("-d"))
destdir = new File(options[i][1]);
CompilerDoclet compiler = new CompilerDoclet();
HandlerInfo[] handlers = compiler.compile(root);
Messenger messenger = new DocletMessenger(root);
HCTablesGenerator generator =
new HCTablesGenerator(getMessageStore(),messenger,destdir);
for(int i = 0;i < handlers.length;i++)
generator.generateHCTables(handlers[i]);
return true;
}
catch(CompilerException e)
{
// no need to display again, it has already been shown
// to the user
return false;
}
}

清单 2 是 compile(ClassDoc) 方法,它抽取类的 HC 信息。Doclet API 是可读的,所以您对理解以下内容应该没有任何问题。例如, ClassDoc.interfaces() 返回类实现的诸接口。 ClassDoc.tags() 返回该类的 Javadoc 标记。

HC 定义了两个类 HandlerInfo 和 MethodInfo 以收集该信息。我选择不直接使用 Javadoc 提供的类,以便使自己不完全依赖于 Javadoc。谁知道呢 ― 将来,我可能想切换到另一个 Java 解析器。

清单 2:compile(ClassDoc)
protected HandlerInfo compile(ClassDoc clasz)
{
ClassDoc[] interfaces = clasz.interfaces();
if(interfaces == null)
return null;
boolean found = false;
for(int i = 0;i < interfaces.length;i++)
if(interfaces[i].qualifiedName().equals("org.ananas.hc.HCHandler"))
found = true;
if(!found)
return null;

Tag[] tags = clasz.tags("xmlns");
NamespaceSupport namespaceSupport = new NamespaceSupport();
if(tags != null)
for(int i = 0;i < tags.length;i++)
{
String content = tags[i].text();
int pos = content.indexOf(' ');
if(pos == -1)
namespaceSupport.declarePrefix("",content);
else
namespaceSupport.declarePrefix(content.substring(0,pos),
content.substring(pos + 1));
}

MethodDoc[] methods = clasz.methods();
List methodsList = new ArrayList();
if(methods != null)
for(int i = 0;i < methods.length;i++)
{
MethodInfo method = compile(methods[i]);
if(method != null)
methodsList.add(method);
}
MethodInfo[] methodsArray = new MethodInfo[methodsList.size()];
methodsList.toArray(methodsArray);

return new HandlerInfo(clasz.qualifiedName(),
namespaceSupport,
methodsArray);
}

还有其它 compile() 方法用于 RootDoc 和 MethodDoc 。 HandlerInfo 和 MethodInfo 也可以在线获得。

表生成器
编写表类是 HCTablesGenerator 的职责。它使用前一篇专栏文章中介绍的 XPathParser 和 DFAFactory 来从 HandlerInfo 创建 DFA。清单 3 展示了相关方法。

您可能感到奇怪,为什么清单 3 中的 compileDFA() 为每个 XPath 创建一个新的 DFA。在上一篇专栏文章中,通过 OR 组合它们。好了,那是我以前提到的问题所在。对此的更详细信息在“由 OR 产生的问题”一节中。

清单 3:编译 DFA
public void generateHCTables(HandlerInfo handler)
throws CompilerException
{
messenger.info(message.getMessage("Compiling",handler.getName()));
try
{
DFATable[] tables = compileDFA(handler);
writeHCTables(handler,tables);
}
catch(IOException e)
{
error("IOException",e.getLocalizedMessage());
}
}

protected DFATable[] compileDFA(HandlerInfo handler)
throws CompilerException
{
XPathParser parser = new XPathParser(handler.getNamespaceSupport(),message);
MethodInfo[] methods = handler.getMethods();
ArrayList array = new ArrayList();
for(int i = 0;i < methods.length;i++)
{
String[] xpaths = methods[i].getXPaths();
for(int j = 0;j < xpaths.length;j++)
{
XPathNode node = parser.axpath(xpaths[j],i,methods[i]);
if(node != null)
array.add(factory.createDFA(node));
}
}
DFATable[] tables = new DFATable[array.size()];
return (DFATable[])array.toArray(tables);
}

writeHCTables() 方法将 DFA 表序列化成一个 Java 类。目前,它只在文本文件中编写 Java 代码。将来,我想要直接编译成字节码。但是,编译成 Java 代码更便于调试。

writeHCTables() 太长(请参阅 清单 4中的摘录)。它是重构的主要候选对象,在 HC 的将来重复中,我肯定会把它分拆成更易于管理的单元。

表类实现了 HCTables 接口(请参阅清单 5)。

清单 5:HCTables
package org.ananas.hc;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

public interface HCTables
{
public static final String CLASS_SUFFIX =
"__org_ananas_hc_tables_1";
public void setHCHandler(HCHandler handler);
public int getCount();
public int move(int xpath,QName qname,int state);
public boolean isAcceptingState(int xpath,int state);
public void acceptStartEvent(int xpath,
int state,
QName qName,
Attributes atts)
throws SAXException;
public void acceptEndEvent(int xpath,int state,QName qName)
throws SAXException;
public void acceptCharactersEvent(int xpath,
int state,
char[] ch,
int start,
int length)
throws SAXException;
}

该接口指定 XPathHandler 和表类之间的约定。 setHCHandler() 用来初始化表类。 getCount() 、 move() 和 isAcceptingState() 定义与 DFA 本身的接口。

最后, acceptXXXEvent() 方法实现应用处理程序中的调用。现在的问题是,由于 XPathHandler 是一个一般类,所以它不知道实际的应用处理程序。因此,它不知道在 DFA 匹配时要调用哪个方法。编译器创建这些方法,它们调用应用处理程序中的正确方法。

因为 Java 反射(Java reflection)的效率不高,所以我有意地选择不使用它。如果您一步步地执行该代码,会看到编译器在创建这些方法时是非常灵活的。例如,它为用户提供了许多对参数的控制。同时,这将禁止用反射来实现,但它在该方法中是完全可接受的。

XPathHandler
剩下的类就是 XPathHandler ,它充当 SAX 事件和 HC 事件之间的代理。清单 6 是处理程序。

构造器将 HCHandler 作为一个参数。它尝试动态地装入相应的表类。因为表类的名称源于应用处理程序的名称,所以这并不太难。名称中的版本号保证了未来的兼容性。

XPathHandler 实现选中的 SAX 事件,并发出对表类的正确调用。 startDocument() 和 startElement() 使它迁移(移动)到下一状态。 endElement() 在读取当前元素前恢复该状态。

清单 6:XPathHandler
package org.ananas.hc;

import java.util.Stack;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class XPathHandler
extends DefaultHandler
{
protected HCTables tables;
protected int[] states;
protected Stack stack;

public XPathHandler(HCHandler handler)
throws HCException
{
try
{
Class handlerClass = handler.getClass(),
tablesClass = handlerClass.forName(handlerClass.getName() +
HCTables.CLASS_SUFFIX);
tables = (HCTables)tablesClass.newInstance();
tables.setHCHandler(handler);
}
catch(ClassNotFoundException e)
{
throw new HCException(e);
}
catch(IllegalAccessException e)
{
throw new HCException(e);
}
catch(InstantiationException e)
{
throw new HCException(e);
}
}

public void startDocument()
throws SAXException
{
stack = new Stack();
QName qname = new QName(QName.ROOT);
states = new int[tables.getCount()];
for(int i = 0;i < tables.getCount();i++)
{
states[i] = tables.move(i,qname,-1);
tables.acceptStartEvent(i,states[i],qname,null);
}
}

public void startElement(String namespaceURI,
String localName,
String qualifiedName,
Attributes atts)
throws SAXException
{
stack.push(states);
QName qname = new QName(QName.ELEMENT,namespaceURI,localName);
int[] cstates = states;
states = new int[tables.getCount()];
for(int i = 0;i < tables.getCount();i++)
{
if(tables.isAcceptingState(i,cstates[i]))
states[i] = tables.move(i,qname,-1);
else
states[i] = tables.move(i,qname,cstates[i]);
tables.acceptStartEvent(i,states[i],qname,atts);
}
}

public void characters(char[] ch,
int start,
int length)
throws SAXException
{
for(int i = 0;i < tables.getCount();i++)
tables.acceptCharactersEvent(i,states[i],ch,start,length);
}

public void endElement(String namespaceURI,
String localName,
String qualifiedName)
throws SAXException

{
QName qname = new QName(QName.ELEMENT,namespaceURI,localName);
for(int i = 0;i < tables.getCount();i++)
tables.acceptEndEvent(i,states[i],qname);
states = (int[])stack.pop();
}

public void endDocument()
throws SAXException
{
QName qname = new QName(QName.ROOT);
for(int i = 0;i < tables.getCount();i++)
tables.acceptEndEvent(i,states[i],qname);
}
}

由 OR 产生的问题
您可能奇怪为什么这个应用程序创建的 DFA 与 XPath 一样多。如果有什么不同的话,那就是这要比只维护一个 DFA 的效率差。假如发生意外问题,这是我所能想到的最佳折中方式。

本专栏文章的初衷是向您展示当项目展开时它是如何运作的。我保证将与您分享我是如何试图解决这些问题和在这一过程中学到的知识。如果有什么不同的话,我希望我错误的开始和遇到的问题将帮助您避免同样的情况。

现在的问题是我有过一些误解。在上一篇专栏文章中,我将 XPath 与一个 OR 运算符链接,以创建单个 DFA。换而言之,我处理下面两个 XPath:

simpara/ulink sect1info/title

就好象它们已写为:

simpara/ulink | sect1info/title

这看上去很好,而且在极大程度上的确是这样。然而,在下列情况下,却不是这样。如果一个 XPath 结束于下一个 XPath 的开始,则这是不正确的。例如,下面两个 XPath:

sect1/simpara simpara/ulink

并不等价于

sect1/simpara | simpara/ulink

您能看出其中的区别吗?这使我花了一点时间。问题是:如果 DFA 识别出 sect1/simpara ,则它不会再探究另一个分支( simpara/ulink )。上面两个 XPath 真正等价于:

sect1/(simpara|simpara/ulink) | simpara/ulink

虽然这是不正确的 XPath 语法,但是括号暗示了不同的优先级。

那么,怎么会这样呢?当我开始寻找算法时,我看了在特定上下文(正则表达式)中的一些示例,而且尝试了将它们与一个完全不同的上下文相匹配。我发现了一些问题(例如,XPath 的符号空间是无限的),但我遗漏了这个问题。

我必须在花几个专栏修正这个算法和用一个不太满意的(并行运行多个 DFA)但较稳定的技术性解决方案发行它之间做出选择。正如我对 XM 所做的那样,为交付本专栏文章中的一个工作版本,而对此方案添加一个自我限制的截止期限,暂停项目,然后去获取一些实际经验。

作为一个技术人员,我倾向于在为利于产生一个更精致的技术性解决方案而忽略截止期限。但是,作为一个顾问,我认识到提早发行一个稳定但较慢的产品是最佳选择。什么也不能违反实际经验,而第二个发行版才是您优化性能的最好机会。

使用 HC
为结束关于 HC 的这一系列,这里给出一个简短的如何操作的指南。 清单 7 是一个 HC 应用处理程序,它将 Docbook 的一个小子集格式化成 HTML。Docbook 是用于技术发布的一种流行 DTD。类为所选的 Docbook 元素定义了方法。 @xpath 标记对 XPath 进行标记。它还实现了 HCHandler 接口(如果 HCHandler 没有定义方法,也没有多少工作量;它本质上只是编译器的一个标志)。

HC 编译器根据名称来区分开始、结束和字符事件。在涉及参数时,这十分灵活。例如,要注意字符方法接受 SAX 字符数组或字符串。

运行 HC 编译器以创建相应的表类:

javadoc 



-docletpath hc.jar;xerces.jar
-doclet org.ananas.hc.compiler.CompilerDoclet



-classpath hc.jar;xerces.jar -sourcepath src
-d autosrc org.ananas.hc.test.*


如果我们一个接一个地复查一下参数,就会发现 -docletpath 是 doclet 的类路径, -doclet 选择 Doclet, -classpath 是应用处理程序的类路径(不要混淆它们),而 -d 是输出目录。

最后一个参数( org.ananas.hc.test.* )选择要使用的包。

用 javac 或 jikes 编译应用程序(包括类文件),然后运行。恭喜,您已一切就绪了。

下一步是什么?
正如我在介绍中提到的,为了获得有关 HC 的实际经验,我打算停止开发 HC。这离完成还远着呢,但是正如我常在本专栏中声明的,我相信从实际出发测试软件以发现需要改进的方面。

我鼓励您尝试一下。请下载并安装 HC,测试它,并在 ananas 讨论邮件列表上报告您的发现。

下一篇专栏文章将启动第三个“使用 XML”项目。与以往一样,新项目将以开放源码发行。

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

请登录后发表评论 登录
全部评论

注册时间:2018-08-23

  • 博文量
    1006
  • 访问量
    728748