2009年8月18日火曜日

実用的なXML: Java NIOへの取り組み バッファーおよびチャネルに寄り道する方法

2002年 6月 01日

http://www.ibm.com/developerworks/jp/xml/library/x-wxxm10/#resources

このコラムは、XIプロジェクトを次のステップへと進めるものです。このコラムでBenoit氏は、新規JavaテクノロジーのAPIに関する (特に、正規表現エンジンとNew I/O (NIOとも呼ばれます) における) 氏の新しい発見について報告しています。XIはまだ作動可能にはなっていませんが、近い将来にどのような姿で登場するのかをかいま見ることができます。
前回の記事では、このコラムのための新規ツール・プロジェクトであるXIを紹介しました。このプロジェクトの課題は、次のとおりです。私の会社では、XMLと、XMLに基づく公開ソリューションであるXMに関する作業グループのWebサイトを運営しています。(XMは「実用的なXML」コラムの最初のプロジェクトです。参考文献を参照してください。)
そのサイトに関する文書の1つに、プロジェクト参加者のリストがあります。このリストは、Eメール・プログラムの住所録として保持されています。その理由は、このコラムでは取り扱いません。ファイル・フォーマットは私が担当しましたが、XMLではありません。それでは、どのようにしてXML公開ソリューションにリストを送り込むのでしょうか?このリストをXMLに変換する必要があります。
もちろん、そのための特別な変換ルーチンを書くのも難しいことではないでしょうが、それは時間のむだのような気がします。それに、XML以外の文書をXMLソリューションに供給しなければならない場合は、ほかにもありそうです。住所録のほかにも、日程表、スプレッドシート、カタログ化情報、およびその他のレガシー・データを処理する必要がありそうです。
XIはこの問題のためのかなり一般的な解決策です。XIは正規表現 (現在はJDK 1.4に組み込まれています) を使用して入力ファイルを構文解析し、対応するXMLファイルを作成します。こうした変換の詳細、およびXIに関する簡単な分析は、前回のコラムで紹介しました (参考文献を参照)。
JDK 1.4の新規機能
私がここでXIを開発することに決めた理由の1つは、JDK 1.4に組み込まれている新しい正規表現 (regex) エンジンを体験してみたかったからです。これから説明しますが、New I/Oパッケージ (一般にNIOと呼ばれ、パッケージjava.nio に入っています) の探求に取り掛かってみると、予想以上の収穫が得られました。
java.regexパッケージ
この正規表現エンジンは、単純明快なインターフェースを備えています。これを使用するためには、基本的に、2つの新規クラスと1つの新規インターフェースを学習する必要があります。新規クラスは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() を呼び出し、引数として正規表現を渡す必要があります。
正規表現初級講座
正規表現は文字列のフォーマットを記述します。最も単純な形式の正規表現では、突き合わせしたいテキストを入力することになります。例えば、ABC という正規表現はABC に一致しますが、DEF には一致しません。
もちろん、正規表現では文字列が厳密に比較されます。例えば、ジョーカー と呼ばれるワイルドカード文字、つまりドット (.) を使用すると、行末の1文字以外のどのような文字とも一致するようになります。したがって、A.C という正規表現は、ABC、AAC、AKC、およびその他多くの文字列に一致しますが、それでもDEF には一致しません。
文字またはジョーカーの後の星印 (*) は、その文字またはジョーカーがいくつ繰り返されていても一致するということを意味します。したがって、A*B という正規表現は、AB、AAB、AAAB、またはAで始まって1つのBで終わる任意の文字列に一致します。
ドットはジョーカーですので、.* は、ABC、IBM developerWorks、およびDEF などの、任意の文字列と一致します。
グループ化演算子として括弧が使用されます。読者もお気付きでしょうが、これを使用すると、文字列からグループの内容を抜き出すことができて、非常に便利です。例えば、リスト1 で使用されている正規表現(*):(.*) は、1つのコロンで区切られた2つの文字列に一致します。
正規表現については、まだ語り残したことがありますので、「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: domainValue:
ananas.org

これは、入力 (domain:ananas.org) が正規表現に一致しているためです。しかし、次のようにして呼び出すと、
java SampleRegex "ananas.org"

次のような出力が得られます。
No match

この入力が正規表現に一致していないためです。
NIO
前のセクション『PatternとMatcher』でCharSequence について触れました。これは、java.langパッケージで文字の配列に関して定義されている新規インターフェースです。String が更新されて、CharSequenceを実装するようになりました。
さらに重要なこととして、NIOパッケージを使用することにより、ファイルを CharSequence としてアクセスすることができます。したがって、Matcher が CharSequence を受け入れますので、ファイル全体に対して正規表現を適用することが可能になりました。これは、java.nioパッケージを調べて分かったことです (参考文献を参照)。
結局、このプロジェクトではjava.nioを使用しないことにしましたが、ソリューションを探すために多くの時間を費やしましたので、これについて説明します。(最後まで突き詰めて追及することは、ソフトウェア開発におけるまたとない気晴らしであり、また、そうしたことをお伝えしなければ、このコラムの本来の意味は失われてしまうでしょう。それに、私の経験を紹介しておけば、読者が同じことを調べなくて済むようになるはずです。)
リスト2 は、ファイルを CharSequence に作り替える方法を示しています。読者は実際には、テキスト・ファイルを基に CharSequence を実装する、CharBuffer という新規クラスを使用することになります。

リスト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の目標は、通常の入出力よりも制御しやすく、柔軟性の高い入出力を提供することです。
NIOが提供するAPIは、通常の入出力のAPIほど抽象的ではありません。例えば、Java IOの場合、バッファー管理のことを考慮する必要がありませんが、その代わり、バッファー管理を制御することもできません。NIOでは、バッファー管理を制御することができます。バッファー管理は、ユーザーが行うようになっているのです!効果的ではありますが、より複雑になると言って差し支えないでしょう。
ここまで書いてきて抱いた印象では、NIOは特に、データベース・エンジン、サーバー、および高性能クライアントなどの、高性能アプリケーションの開発者に向いているのではないかと思います。通常のプログラミングでこれを使う理由 (そしてそれに伴う追加作業を行う理由) は見当たりません。
さらに、XIにおける私の最終目標は、SAXで定義された XMLReader との互換性を維持することです。XMLReader は現在 InputStream または Reader では使用できますが、NIOでは使用できません。私は、InputStream を CharBuffer に変換するための完全に汎用的なソリューションを見付けることができませんでした。(部分的なソリューションは見付かりました。)よいアイデアをお持ちの方は、ぜひEメールでお知らせください。次回の記事で紹介させていただきます。
最終的に私は、通常の入出力を使用して、ファイルを1行ずつ文字列に読み込み、それらの文字列に正規表現を適用することにしました。




上に戻る


機能の仕組み
何ができなかったかということについての話は、もう十分でしょう。私がやり遂げることのできた ことをご披露しましょう。分析を行う過程で、私は、ファイルを記述するための単純なデータ構造 (および対応する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 に示してあります) を書くことができます。この XIReader ではXML文書は書かれませんが、正規表現を使用してテキスト・ファイルをその構成要素に分解するための論理は含まれています。
この再帰的アルゴリズムは read() メソッドで使用されています。XML文書の階層構造が本来再帰的なものであるため、再帰的アルゴリズムは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.rine();
}
}
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 の稼働バージョンが出来上がる予定です。残っている作業は、リスト7 の System.out.println() を ContentHandler への適切な呼び出しに置き換えることだけです。また、XMLReader インターフェースの完全な実装にも取り掛かる必要がありますが、これはさほど難しいことではありません。
このコラムがよい例ですが、新規ライブラリーの学習には、多くの時間を要することがあります。


参考文献
Regex for Java を入手してください。これは、IBMによるもう1つの正規表現エンジンです。

MEC-Eagle を参照してください。これも、レガシー・ファイルをXMLにインポートすることのできる、e-commerceアプリケーション用のツールです。

Wordまたはその他のワード・プロセッサー文書をお持ちの読者は、upCast を参照してください。

正規表現に関する有益な参考書として、『Mastering Regular Expressions』(Jeffrey E. F. Friedl著、O'Reilly発行、1997年) をお勧めします。

java.nioの詳細については、http://java.sun.com/j2se/1.4/docs/api/java/nio/package-summary.html を参照してください。

XIで提供される変換の詳細については、Benoit Marchalの前回のコラム実用的なXML: XIでテキストをXMLとしてインポート (developerWorks、2002年4月) を参照してください。

著者の最初のプロジェクトであるXMについては、「実用的なXML」コラムのこれまでの記事を読み返してください。
実用的なXML: コンテンツ・マネージメントにXSLTを使用する (developerWorks、2001年7月)
実用的なXML: リンク・マネージメントと将来への準備 (developerWorks、2001年8月)
実用的なXML: 処理命令およびパラメーター (developerWorks、2001年9月)
実用的なXML: XMバージョン1のまとめ (developerWorks、2001年10月)

XMLおよび関連テクノロジーにおけるIBM認定開発者になる方法を探してください。

developerWorks XMLゾーン には、ほかにも多くのXML関連の参考文献が掲載されています。


著者について



Benoit Marchal氏は、ベルギーのナミュールを拠点にしたコンサルタントおよび著述家です。彼の著作には、 XML by Example(Que社、邦訳: インプレス社「実例で学ぶXML」。間もなく第2版が出版される予定です)、 Applied XML Solutions および XML and the Enterprise があります。また、Gamelanのコラムや、developerWorks XML zoneのコラムWorking XML の著者でもあります。最新プロジェクトの詳細については、www.marchal.com をご覧ください。

0 件のコメント:

マイブログ リスト


Jang ki hote

自己紹介