レベル: 中級 Benoit Marchal (bmarchal@pineapplesoft.com), Consultant, Pineapplesoft
2002年 7月 01日
コラムニストのBenoit Marchal氏は、レガシー・テキストをXMLに変換するオープン・ソース・プロジェクト、XIの仕上げを続けます。効率を高めるために、XIはSAX XMLReaderインターフェースを実装するようになりました。これによりXIをXSLT処理プログラムに容易にリンクすることができます。コード・サンプルでそれらの技法が示され、また、完全なソース・コードも入手することができます。このコラムでは毎月、XML開発者、特にJavaテクノロジーに携わっている皆様を手助けするために著者が設計した、オープン・ソース・プロジェクトについて報告を行っています。
前回の2つのコラムでは、レガシー・ファイルをXMLに変換するためのプロジェクトである、XI (XML Importを縮めたものです) に取り組みました (参考文献 を参照)。XIを開発する動機となったのは、XMLサイトの一部として住所録を公開する必要があったことです。住所録はEメール・クライアントの独自の形式で保持されるため、テキストをXMLに変換するためのツールが必要でした。
たまたま、JDK 1.4に組み込まれた新しい正規表現ライブラリーを試す機会がありました。正規表現は柔軟な変換ソリューションに適しています。レガシー文書の構文解析方法を示すために、変換ルーチンをハード・コーディングする代わりに、正規表現のセットを使用することができます。住所録のために一連の規則を使用しますが、他のカレンダー、化学分析データ、Webサーバー・ログ、あるいはその他の形式については、別の規則を作ることになりそうです。XIはさらに汎用的なツールであって、多くのプロジェクトで使用したり再利用したりすることができます。
XMLに戻りましょう
前回のコラム「Java NIOへの取り組み」(参考文献を参照) では、正規表現ライブラリーの研究にかなりの時間を割きました。私の想定のいくつかは、完全に的外れであることが分かりましたが、それでも私は、正規表現を使用して住所録をエレメントに構文解析することができました。
私が目指しているのは汎用ソリューションですので、規則のセットを保持するための小さなデータ構造を作成しました。このデータ構造は、基本的にXMLタグ名を正規表現に関連付けるものです。テスト用の固定データ構造の作成だけしか行いませんでしたが、コードを編成することにより、ファイルからこのデータ構造にデータを埋める簡単な機能に仕上げ、それを実装することができました。
このコラムでは、主として、コードを整理して、有効なXML文書を作成できるようにします。また、既存のアルゴリズムをXMLパーサーとしてパッケージする作業を行いました。後で示しますが、XMLパーサー・インターフェースは、XSLT処理プログラムを扱うには便利です。
XML文書を作成する最良の方法
XIを仕上げるための最も簡単なソリューションは、コードを見直して、XMLタグを作成するようにさまざまなprintステートメントを作り替えることではないでしょうか。確かに、文書を構文解析してXMLエレメントをノードに関連付ける論理は、すでに存在します。例えば、このアルゴリズムは正規表現と一致した場合、それに関連する次のようなエレメントを出力します。
System.out.print(ruleset.getMatchAt(i).getQualifiedName());
|
これを作り替えて適切なXMLを作成するようにすることは、難しくありません。
System.out.print("<"+ruleset.getMatchAt(i).getQualifiedName()+">"); |
もちろん、上のステートメントは開始タグだけしか印刷しませんので、終了タグと内容のための印刷ステートメントをさらに追加する必要がありますが、これは難しい作業ではありません。
このソリューションは最も手間がかからない方法ですので、もしも私がXML文書の作成だけに興味を持っていたのであれば、おそらくこの方法を選んでいたことでしょう。不等号括弧、アンパーサンド、およびその他の予約文字を拡張するには、特別な注意が必要ですが、それはささいなことです。また、XML文書をコンソールに出力する代わりにファイルに保管する必要もありますが、その手間も取るに足りないものです。
だからといって、XML文書をファイルに書き込むことで満足するわけにはいきません。これまでのコラムで述べたように、XIからの出力を直接使用するつもりはありません。これまでの経験から明らかなことですが、レガシー文書を再編成しなければならないことがよくあります。例えば、この住所録の場合、alias 行とnote 行を結合する必要があります。こうしたケースやその他の類似したケースを処理する論理をXIに追加することもできますが、インポート・プロセスを次の2つのステップに分割するほうが有利であることが分かりました。
構文変換は、テキスト情報を取り込んで、それをごく単純なXML構造でラップします。一般には、これによって作られたXML文書はオリジナルの文書にきわめて近いものになります。ほとんどの場合、区切り文字をXMLタグで置き換える程度の単純な変換しか行われません。XIが行うのは、そのようなことです。
2番目のステップでは、未完成のXML文書をターゲット・ボキャブラリーに作り替えるための変形を行います。この目的には、強力な変形言語を備えているXSLTが特に適していることが分かりました。また、XSLTは標準であるため、エディターなどのサポート・ツールが完備しています。
要するに、私は必ずしもXIでXML文書をファイルに書き込みたいわけではないのです。むしろ、XSLT処理プログラムとインターフェースするように最適化したいと考えています。JDK 1.4には、ファイル (ストリーム)、SAXイベント、およびDOMツリーからの入力を受け入れるApache Xalanが同梱されています。個人的には、3つのインターフェースの中でSAXが気に入っています。
SAXの魅力は、プログラミングが行いやすく、XML文書を処理するためにかなり有効なインターフェースを備えている点です。ファイルと比較すると、一時ファイルへの書き込みが少なく、DOMと比較すると、必要なメモリーが少なくて済みます。
SAXインターフェースのプログラミング
この記事の残りの部分では、読者がSAXプログラミングをよく理解していることを前提に話を進めます。SAXプログラミングに詳しくない読者は、developerWorks にある「強力なAPI、SAX」をお読みください (参考文献を参照)。
SAXにおける最も重要なインターフェースは、XMLReader とContentHandler の2つです。XMLReader は、XMLパーサーの初期化方法と開始方法を記述し、ContentHandlerは、XML文書の構文解析時にXMLReader が発生させたイベントをリストします。
読者の皆さんはおそらく、XML文書を読み取るときにこれら両方のインターフェースを使用したことがあると思います。ただし、これらのインターフェースに精通している場合でも、このアプリケーションを使用するためには、やや異なる角度からSAXを見る必要があります。この場合、私は、SAXのユーザーではなく、独自のパーサーを作成していることになります。厳密に言うと、XIはXMLパーサーではありません。XIはXML文書を読み取りません。しかし、テキスト文書を読むためのXMLビューを提供しますので、XMLReader インターフェースに準拠させることができます。
XIによるSAXの実装は、XIReader クラスで行われます。このクラスは大規模なため、ここでそのすべてを示すことはできません。先に進む前に、developerWorks のOpen Sourceセクションからコピーを入手することをお勧めします (参考文献を参照)。
XIReader は、SAXインターフェースの実装および 実際のテキスト構文解析とXML文書の生成の、2つの作業を行います。リスト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 は、ContentHandler、ErrorHandler、DTDHandler、およびEntityResolver の各SAXハンドラーを登録して利用するメソッドを提供します。
厳密に言うと、DTDHandler とEntityResolver は役に立ちません。レガシー・テキストにはDTDがないため、XIReader がDTD関連のイベントを発生させることはありません。
同様に、EntityResolver も必要ありません。前に述べたように、パーサーは、最上位の文書エンティティーについてはEntityResolver を使用しないことになっています。このインターフェースが役に立つのは、DTDなどの外部エンティティーの場合だけです。やはりこれも、レガシー・テキスト文書には使用されません。それでも、SAXはメソッドに対して、これら2つのハンドラーのsetとgetを命じ、XIReader はそれに従っています。
XIReader は、SAXのフィーチャーおよびプロパティーに関する限定的なサポートも実装しています。フィーチャーおよびプロパティーは、構文解析のさまざまな局面を制御するもので、http://xml.org/sax/features/namespaces のようなURLによって識別されます。これらのURLはIDとしてのみ機能しますので、これらを解決しようとしないでください。(Webサイトを訪問しないでください。訪問の対象となるようなWebサイトはありません。)
仕様によると、XMLReader は、http://xml.org/sax/features/namespaces フィーチャーのtrue への設定をサポートしなければならず (false のサポートはオプションです)、また、http://xml.org/sax/features/namespace-prefixes のfalse への設定をサポートしなければなりません (true はオプションです)。
最初のフィーチャーは、パーサーがXMLネームスペースをデコードする (true の場合) のかどうかを制御します。XIReader は常にネームスペースを使用します。2番目のフィーチャーは、属性リスト内のネームスペース宣言を報告する (true の場合) のかどうかを制御します。XIReader は両方の値をサポートします。
つまり、XIReader では、仕様への最低限の準拠が行われています。ただし、http://xml.org/sax/features/namespace-prefixes の (仕様で要求されているfalse への設定だけではなく)true への設定をサポートしなければならないことが分かりました。このプロパティーをtrue に設定しなければ、Apache Xalanがネームスペースを正しく処理できないためです。
仕様では、その他のフィーチャーとそのURLも定義されていますが、それらのサポートにはパーサーは必要ありません。これらのフィーチャーのほとんどは、妥当性検査とXMLスキーマを処理するものであるため、私はそれらを無視することにしました。
私はまた、パーサーのルール・ファイルを用意するために、新規プロパティーhttp://ananas.org/xi/features/rulesets も定義しました。このプロパティーは、ルール・ファイルを指し示すInputSource 値を受け入れます。
ContentHandlerと構文解析
前回のコラム「Java NIOへの取り組み」(参考文献を参照) で示したコードでは、大半の処理がread() というメソッドで行われていました。私は、読みやすさを向上させるためにこのメソッドの名前をmatch() に変更し、また、入力文書のデコード時にContentHandler を呼び出すように改良しました。リスト2 はその様子を示しています。このコードを「Java NIOへの取り組み」のコードと比較すると、非常によく似た構造になっていることが分かると思います。重要な違いはただ1つ、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(i).matches(st))
{
Match match = ruleset.getMatchAt(i);
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));
}
|
実用的なXML コラムの第1回で紹介したパブリッシング・プロジェクトXMをご記憶の読者は、ContentHandler イベントを発生させることの意味はよくお分かりのはずです。XMでは、未解決のハイパーリンクを修正するためにこれを行いました。XIReader は同じ論理を基にしたものですが、これよりもさらに野心的なことを目指しています。リンクに関して1つのイベントを発生させる代わりに、文書全体を記述するのに十分なだけのイベントを発生させるようになっています。
正直なところ、最初のうちはXMLReader の完全実装が書けるのかどうか、不安に思っていました。しかし、このコラムが示しているように、やってみると驚くほど簡単でした。SAXという名前がSimple API for XMLに由来しているだけのことはあります。
ContentHandler の使用方法はきわめて単純です。通常の方法では、開始タグと終了タグ、および内容を印刷するためのメソッドを用意する必要があります。これらのメソッドは、文字の拡張、字下げ、およびその他の構文関連の問題を扱います。ContentHandler は基本的に、それらのメソッドをユーザーに代わって定義してくれます。開始タグを印刷するにはstartElement() メソッドを使用し、終了タグを印刷するにはendElement() メソッドを使用し、内容を印刷するにはcharacters() メソッドを使用してください。
ルール・ファイルの読み取り
XMLReader ができあがった後で、XIでルール・ファイルを読み取れるようにしたくなりました。すでに、住所録以上の機能を備えたXIのアプリケーションができていたこともあって、ハード・コーディングされた正規表現の制約から逃れられるようにしたいと考えました。
前々回のコラムで紹介したボキャブラリーは、ほとんど保存されていました。ルール・ファイルは、リスト3 のようなものになります。ルート・エレメントはrules であって、これには1つまたは複数の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 のボキャブラリーは、オリジナルのものに1個所変更が加えられたものです。これにより、この文書は、ルール・ファイル全体に適用される1つのグローバル・ネームスペースをサポートするようになりました。当初のアイデアでは、ユーザーがルール・ファイル内で複数のネームスペースを指定するようになっていました。しかし、それではXIReader が必要以上に複雑なものになってしまいます。
この問題を研究するうちに、グローバル・ネームスペースが、必要な事態のうちの99%に適用できることが分かりました。しかし、本当に複数のネームスペースが必要になった場合は、どうすればよいのでしょうか?そのような場合でも、文書はXSLTで後処理されますので、対処することができます。スタイル・シートに新しいネームスペースを追加することは、簡単に行えます。
救援HC
このコラムを書く楽しみの1つに、話を進めながらプロジェクトを再利用できることがあります。今回は、ルール・ファイルの構文解析を簡素化するために、数か月前に紹介したHCというハンドラー・コンパイラーを使用します。
該当のコラムを読み逃した方のために説明しておくと、HCは、XPathで注釈が付けられたJavaクラスを受け入れて、それをSAXのContentHandler に変換するプリコンパイラーです。クラスに属する各メソッドは、1つまたは複数のXPathと一致します。実際、これにより、たくさんの状態管理コードを書くという退屈な作業が軽減されます。
リスト4 は、ルール・ファイル用のハンドラーです。こうしたXPathは、Javadocコメントで使用することができます。このハンドラーは、ルール・ボキャブラリー内でそれぞれのエレメントごとにメソッドを1つ定義します。そして、ルール・ファイルを読み進みながら、データ構造に正規表現を適用していきます。
リスト4: RulesHandler.java
package org.ananas.xi;
import java.util.*;
import org.xml.sax.*;
/**
* @xmlns xi http://ananas.org/2002/xi/rules
*/
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の作業は完成に近づいています。実際に動く処理プログラムはすでにできていて、それをXSLT処理プログラムとインターフェースさせる作業も、リスト5 に示すように簡単に行えます。次回のコラムでは、既存のコアを単純なユーザー・インターフェースで包み込んで、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"));
}
|
参考文献
著者について  | 
|  | Benoit Marchal氏は、ベルギーのナミュールを拠点にしたコンサルタントおよび著述家です。彼の著作には、 XML by Example(Que社、邦訳: インプレス社「実例で学ぶXML」。間もなく第2版が出版される予定です)、 Applied XML Solutions および XML and the Enterprise があります。また、Gamelanのコラムや、developerWorks XML zoneのコラムWorking XML の著者でもあります。最新プロジェクトの詳細については、www.marchal.com をご覧ください。 |
記事の評価
|