我的上一篇文章介绍了 XI,它是本专栏的一个新的工具项目。我面临的挑战是这样的:我的公司使用 XML 和 XM(一种基于 XML 的发布解决方案)维护着一个工作组的网站。(XM 是“使用 XML”专栏的第一个项目。请参阅 参考资料。)
用于那个站点的其中一个文档是一张参与者列表。由于那方面的内容已不属于本专栏的范围,所以用电子邮件程序将该列表作为通讯录来维护。令我头疼的是,这个文件的格式不是 XML。所以,我如何把它用于 XML 发布解决方案?我需要将这张列表转换成 XML。
编写一个专门的转换例程当然不困难,但这看起来象在浪费时间:因为还有其它需要把非-XML 文档用于 XML 解决方案的情况。除了通讯录,我可能还需要处理议程、电子表格、目录信息以及其它旧数据。
针对这个问题,XI 提供了一种普遍适用的解决方案。它使用正则表达式(现在已内置于 JDK 1.4 中)来解析输入文件并创建一个 XML 副本。我的上一篇专栏文章介绍了这些转换的详细信息以及 XI 的简短分析(请参阅 参考资料)。
JDK 1.4 中的新特性
我决定现在开发 XI 的原因之一是想要用已构建到 JDK 1.4 中的新的正则表达式( regex
)引擎进行实验。正如您将看到的,我所获得的远远超出了我在开始探究新 I/O 包(通常称为 NIO,它在包 java.nio
中)时所期待的。
包 java.regex
该正则表达式引擎有一个简单且清晰的接口,它主要要求您了解两个新类和一个新接口。这两个新类是 Pattern
和 Matcher
,而新接口是 CharSequence
。(后者可能会产生问题,我将在后面讨论它。) 清单 1 说明了如何使用 Pattern
和 Matcher
。
清单 1. 使用正则表达式引擎
import java.util.regex.*;
public class SampleRegex
{
public static void main(String[] params)
{
Pattern pattern =
Pattern.compile("(.*):(.*)")
;
Matcher matcher =
pattern.matcher(params[0])
;
if(
matcher.matches()
)
{
System.out.print("Key:");
System.out.println(
matcher.group(1)
);
System.out.print("Value:");
System.out.println(matcher.group(2));
}
else
System.out.print("No match");
}
}
|
Pattern
是一个正则表达式编译器。它接受一个正则表达式并将它编译成 Matcher
。 Matcher
用于将正则表达式应用到字符串中,或者更确切地说,是应用到 CharSequence
中。
Pattern
没有公共的构造函数。要创建 Pattern
,必须调用它的 compile()
,同时传递一个正则表达式作为参数。
正则表达式 101
正则表达式描述字符串的格式:使用最简单的正则表达式格式,您可以输入您希望匹配的文本。例如,正则表达式 ABC
将与字符串 ABC
匹配,但不与 DEF
匹配。
当然,正则表达式被限定为严格的字符串比较。例如,可以使用称为 百搭(joker) ― 一个句点( .
)― 的通配符,它可以与一行中除了最后一个字符以外的任何一个字符相匹配。所以,正则表达式 A.C
将与字符串 ABC
、 AAC
、 AKC
以及许多其它字符串相匹配,但它依然不与 DEF
相匹配。
字符或百搭后面的星号( *
)表明它可以与多个任意字符相匹配。因此,正则表达式 A*B
将与字符串 AB
、 AAB
、 AAAB
或以 A 开头并以单个 B 为结尾的任何其它字符串相匹配。
由于句点是一个百搭,所以 .*
将与包括 ABC
、 IBM developerWorks
和 DEF
在内的任何字符串相匹配。
圆括号用作分组操作符。它特别方便,因为正如您将看到的,从字符串中提取出一个组的内容是可能的。例如, 清单 1 中使用的正则表达式 (.*):(.*)
与由冒号分隔开的两个字符串相匹配。
有关正则表达式还有许多内容,我推荐您参考象 Mastering Regular Expressions那样的参考书(请参阅 参考资料)。
Pattern 和 Matcher
在 清单 1 中,一旦将正则表达式编译成 Pattern
,它就创建了带 matcher()
方法的 Matcher
。 Matcher
接受 CharSequence
(在下一节 NIO 中将对此作更详细的讨论)并报告正则表达式是否匹配。 Matcher
提供了几个方法来测试正则表达式: matches()
、 lookingAt()
和 find()
。每个方法都可以应用正则表达式,但方式不同。
Matcher
还提供了 group()
来检索与给定组相匹配的字符串。从 1 到 n 对组进行编号,而 group(0)
是一个完整的正则表达式。
清单 1将正则表达式应用到命令行参数中。它打印找到的组(如果找到的话)。例如,用下面这条命令调用此应用程序:
java SampleRegex "domain:ananas.org"
|
这时,它将打印:
Key: domain
Value: ananas.org
|
因为输入( domain:ananas.org
)与该正则表达式相匹配。然而,如果用下面这条命令调用此应用程序:
java SampleRegex "ananas.org"
|
它将打印:
因为输入不与正则表达式相匹配。
NIO
我在前一节 Pattern 和 Matcher 中提到过 CharSequence
。这是定义在 java.lang 包中用于字符数组的新接口。对 String
已进行了更新以便实现 CharSequence
。
更重要的是,通过使用 NIO 包,将一个文件作为 CharSequence
进行访问是可能的。因此,由于 Matcher
接受 CharSequence
,所以将正则表达式应用到全体文件是可能的。这就是我研究完 java.nio 包后得出的结论(请参阅参考资料)。
最终,在这个项目中我没有使用 java.nio,但我仍然要讨论它,因为我在寻找解决方案时花了许多时间。(在软件开发中,钻牛角尖是一项非常有趣的消遣,如果我不对此进行报告,那么本专栏本身就不真实。另外,我希望我的经历将为您节省一些时间以避免进行相同的研究。)
清单 2 向您演示了如何将文件转换成 CharSequence
。实际上,您最终可以使用 CharBuffer
这个新类,它对文本文件实现 CharSequence
。
清单 2. 使用 CharBuffer
FileInputStream input = new FileInputStream(params[0]);
FileChannel channel = input.getChannel();
int fileLength = (int)channel.size();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY,0,fileLength);
Charset charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
Matcher matcher = pattern.matcher(charBuffer);
// ...
|
在进一步讨论之前,我必须承认我不能确定我已经充分掌握了 NIO 背后的逻辑。我只是在最近才开始使用这个 API,这里只是我所掌握的。
正如您看到的,将文件转换成 CharBuffer
需要许多对象。这就是使用新 API 时的模式。就我所理解,NIO 的目的似乎是为了给您比常规 I/O 更多的控制和更大的灵活性。
NIO 没有太多抽象的 API。例如,使用 Java IO,不必担心缓冲区管理,但您也不能对它进行控制。而 NIO 向您提供了对缓冲区管理的更多控制 ― 通过让您运行它!可以论证的是,它更有效,但它也更复杂。
当我在写这篇专栏文章时,我对 NIO 的印象是,它将对开发诸如数据库引擎、服务器和高性能客户机的高性能应用程序的人员特别有用。而对于常规编程,我认为没有必要使用 NIO(因为这要花额外的精力)。
而且,我使用 XI 的最终目标是使它与 XMLReader
保持兼容,因为它是由 SAX 定义的。 XMLReader
通常使用 InputStream
或 Reader
,但不使用 NIO。我没有找到一种完全普遍使用的解决方案来将任何 InputStream
转换成 CharBuffer
。(但我确实找到了部分解决方案。)如果您有更好的想法,请给我发电子邮件,我将在下一篇专栏文章中报告它。
最后,我决定使用常规 I/O,将文件逐行地读取成字符串,并将正则表达式应用到那些字符串中。
工作原理
我已经做了许多无用功;现在让我们看一下我 已完成的工作!在分析过程中,我定义了一个简单的数据结构(以及相应的 XML 词汇表)用于文件的描述。为了用 XI 处理新文件,我编写了另一个描述。
我通过使用下列文件实现数据结构着手:
Ruleset
(请参阅 清单 3)表示一组正则表达式。 Match
(请参阅 清单 4)表示一个正则表达式。该类封装了 java.regex
。 Group
(请参阅 清单 5)是来自正则表达式的组(加圆括号的表达式)。
清单 3. Ruleset.java
package org.ananas.xi;
import java.util.*;
public class Ruleset
extends QName
{
private List matches = new ArrayList();
private String error = null;
public Ruleset(String namespaceURI,
String localName,
String qualifiedName)
{
super(namespaceURI,localName,qualifiedName);
}
public void setError(String error)
{
this.error = error;
}
public String getError()
{
return error;
}
public synchronized void addMatch(Match match)
{
matches.add(match);
}
public synchronized Match getMatchAt(int index)
{
return (Match)matches.get(index);
}
public synchronized int getMatchCount()
{
return matches.size();
}
}
|
Ruleset
实质上是一个用于 Match
对象列表的容器。
清单 4. Match.java
package org.ananas.xi;
import java.util.*;
import java.util.regex.*;
public class Match
extends QName
{
private Pattern pattern;
private Matcher matcher = null;
private String input = null;
private List groups = new ArrayList();
public Match(String namespaceURI,
String localName,
String qualifiedName,
String pattern)
{
super(namespaceURI,localName,qualifiedName);
this.pattern = Pattern.compile(pattern);
}
public synchronized void addGroup(Group group)
{
groups.add(group);
}
public synchronized Group getGroupNameAt(int index)
{
if(index < 1 || index > groups.size())
throw new IndexOutOfBoundsException("index out of bounds");
return (Group)groups.get(index - 1);
}
public synchronized String getGroupValueAt(int index)
throws IllegalStateException, IllegalArgumentException
{
if(matcher == null)
throw new IllegalStateException("Call matches() first");
return getGroupNameAt(index).isText() ?
matcher.group(0) : matcher.group(index);
}
public synchronized int getGroupCount()
{
return groups.size();
}
public boolean matches(String st)
{
input = st;
if(matcher == null)
matcher = pattern.matcher(st);
else
matcher.reset(st);
return matcher.lookingAt();
}
public String rest()
{
if(matcher == null)
throw new IllegalStateException("Call matches() first");
int end = matcher.end(),
length = input.length();
if(end < length)
return input.substring(end,length);
else
return null;
}
}
|
Match
是这个数据结构中最重要的类。它表示一个正则表达式并提供了使正则表达式与字符串相匹配的逻辑。注:它使用 lookingAt()
来应用正则表达式。因为 lookingAt()
能与部分字符串相匹配,所以将字符串分解成子串是可能的。
清单 5. Group.java
package org.ananas.xi;
public class Group
extends QName
{
public Group(String namespaceURI,
String localName,
String qualifiedName)
{
super(namespaceURI,localName,qualifiedName);
}
}
|
所有类均由 QName
(请参阅 清单 6)派生而来, QName
将 XML 元素名称表示成名称空间 URI 和本地名的组合。
清单 6. QName.java
package org.ananas.xi;
public class Group
extends QName
{
public Group(String namespaceURI,
String localName,
String qualifiedName)
{
super(namespaceURI,localName,qualifiedName);
}
}
|
使用它
虽然在本专栏文章发表时,还没有及时完成 XIReader
的第一版(我们的软件开发人员对我们的能力总是充满希望和自信,但在开发过程中他们常常会遇到一些问题,特别是在了解新库时),但是我可以编写一个简单的测试类,它使我可以对正则表达式 API ― 该类显示在 清单 7中 ― 进行试验。虽然它没有编写 XML 文档,但它已包含了通过正则表达式将文本文件分拆成其组成部分的逻辑。
在 read()
方法中可以找到递归算法。XML 文档非常适合使用递归算法,因为它们的层次结构在本质上是可递归的。该算法如下:
- 给定一个字符串,它对
Match
进行循环以试图找到相应的正则表达式。 - 对于连接到
Match
的每个 Group
,它打印其内容。 - 如果
Group
名称与另一个 Ruleset
相匹配,那么递归调用就尝试进一步分解字符串(对于上一篇专栏文章示例中的 an:fields
元素,也是这么做的)。 - 如果正则表达式没有完全处理完字符串,那么对未处理完的字符串继续应用递归调用。
清单 7. Test.java
package org.ananas.xi;
import java.io.*;
import java.util.regex.*;
public class Test
{
public static void main(String[] params)
throws IOException
{
Ruleset[] rulesets = getRulesets();
BufferedReader reader = new BufferedReader(new FileReader(params[0]));
String st = reader.readLine();
while(st != null)
{
read(rulesets,st);
st = reader.readLine();
}
}
public static Ruleset[] getRulesets()
{
Ruleset[] rulesets = new Ruleset[2];
rulesets[0] = new Ruleset("http://ananas.org/2002/sample",
"address-book",
"an:address-book");
rulesets[1] = new Ruleset("http://ananas.org/2002/sample",
"fields",
"an:fields");
Match match = new Match("http://ananas.org/2002/sample",
"alias",
"an:alias",
"^alias (.*):(.*)$");
Group group = new Group("http://ananas.org/2002/sample",
"id",
"an:id");
match.addGroup(group);
group = new Group("http://ananas.org/2002/sample",
"email",
"an:email");
match.addGroup(group);
rulesets[0].addMatch(match);
match = new Match("http://ananas.org/2002/sample",
"note",
"an:note",
"^note .*:(.*)$");
group = new Group("http://ananas.org/2002/sample",
"fields",
"an:fields");
match.addGroup(group);
rulesets[0].addMatch(match);
match = new Match("http://ananas.org/2002/sample",
"fields",
"an:fields",
"[//s]*<([^<]*)>");
group = new Group("http://ananas.org/2002/sample",
"field",
"an:field");
match.addGroup(group);
rulesets[1].addMatch(match);
return rulesets;
}
public static void read(Ruleset[] rulesets,String st)
{
read(rulesets,rulesets[0],st,false);
}
public static void
read(Ruleset[] rulesets,Ruleset ruleset,String st,boolean next)
{
boolean found = false;
for(int i = 0;i < ruleset.getMatchCount() && !found;i++)
{
if(ruleset.getMatchAt(i).matches(st))
{
found = true;
Match match = ruleset.getMatchAt(i);
if(!next)
{
System.out.print(ruleset.getMatchAt(i).getQualifiedName());
System.out.print(' ');
}
for(int j = 1;j <= match.getGroupCount();j++)
{
String qname = match.getGroupNameAt(j).getQualifiedName();
boolean deep = false;
for(int k = 0;k < rulesets.length && !deep;k++)
if(rulesets[k].getQualifiedName().equals(qname))
{
System.out.print("/n >> /"");
System.out.print(match.getGroupValueAt(j));
System.out.print("/" >> ");
read(rulesets,rulesets[k],match.getGroupValueAt(j),false);
deep = true;
}
if(!deep)
{
System.out.print(match.getGroupNameAt(j).getQualifiedName());
System.out.print(' ');
System.out.print(match.getGroupValueAt(j));
System.out.print(' ');
}
}
String rest = match.rest();
if(rest != null)
read(rulesets,ruleset,rest,true);
}
}
System.out.println();
}
}
|
不要被 getRulesets()
方法所困扰。它暂时在内存中创建了一个文件描述。在下一次迭代中,它将从 XML 文件中读取该文件描述。
朝着 XIReader 前进
我很快将有 XIReader
的工作版本。这里遗留的工作是用适当的 ContentHandler
调用来替代 清单 7 中的 System.out.println()
。另外还需要开始完整地实现 XMLReader
接口,但这并不特别困难。
正如本专栏文章清楚说明的那样,学习新库需要花许多时间。