レベル: 初級 Benoit Marchal (bmarchal@pineapplesoft.com), Consultant, Pineapplesoft
2002年 3月 01日 このコラムで、Benoitは、Handler Compiler (HC) に対するフロントエンドを作成し、DFAについての予想外の問題に直面しています。安定してはいますが最善とはいえないソリューションを利用して、さらなるテストに向けてHCの最初のバージョンをリリースすることができました。
Handler Compiler (HC) を巡る開発の今シリーズの最終回となるこのコラムでは、HCの最初の稼動バージョンをお届けします。このコラムの最初のプロジェクトであったXMのときと同じように、これから何か月かの間は、HCのフィールド・テストを行いながら、別のプロジェクトに着手するつもりです。
この時間を利用してプロジェクトからさらに経験を得て、今後の開発に対する要件を洗い出したいと思います。もちろんその間も、読者に皆さんには、ぜひHCをダウンロードしてそれぞれの環境でテストし、ananas-discussionメーリング・リストで意見を交換していただきたいと思います。バグ修正および更新の情報は、CVSサーバーで提供します (参考文献を参照)。
現在の状況
HCは、SAXコードの作成に関する私の経験から生まれました。SAXパーサーの威力と柔軟性は評価できますが、一方で、ドキュメント内でのパーサーの位置を追跡するには、退屈なコーディングを長々と行う必要があることもわかりました。HCは、XPathから状態追跡コードを自動的に生成します。
HCは、2つのコンポーネントで構成されています。1つは、アプリケーション・ハンドラー を受け取ってテーブル・クラス を作成するコンパイラーです。アプリケーション・ハンドラーは、HCHandlerインターフェースを実装するJavaクラスです。特殊なJavadocコメントを使って、パーサーがXPathを対応付けるときに呼び出すメソッドを示します。コンパイラーは、開発においてだけ使用します。
第2のコンポーネントはランタイムです。ランタイムの最も重要なクラスはXPathHandler です。XPathHandler は、アプリケーション・ハンドラーに対する呼び出しの中でSAXイベント (エレメント開始、エレメント終了など) を解釈するプロキシーとして機能します。ランタイムはアプリケーションに付属しています。
図1は、HCランタイムのクラス・モデルです。この中で、アプリケーション・ハンドラーはHCCountHandlerです。
図1. HCランタイムのクラス・モデル
前回のコラムでは、いわゆるDFA (Deterministic Finite Automaton : 決定性有限オートマトン) で一群のXPathをコンパイルするためのロジックを作成しました。前回のコラムでの説明を繰り返すことはしませんが、DFAとは、XPathを効率よくコンパイルできる構造です。
残っているのは、DFA構成アルゴリズムとJavadocを結び付けて、アプリケーション・ハンドラーのソースからXPathを取り出すことです。このコラムでは、テーブル・クラスを記述する必要もあります。
プロジェクトの最後まで何の障害もなくたどり着けると期待していたのですが、残念ながら、DFAに問題が見つかったため、考えていたよりも複雑なコーディングを行うことになりました。詳しくは後で説明します。
JavadocのDoclet
JavaクラスにHCをスムーズに統合するために、旧友であるJavadocコメントの力を借りました。新しいJavadocタグである@xpath は、次に示すように、メソッドが特定のXPathと一致していなければならないことを示します。
/**
* @xpath para
*/
public void startPara()
{
writer.print("<p>");
}
|
注意: ここでは、タグという言葉ば2種類の使われ方をしています。JavadocのタグはJavaコードの中で使用し、@name value という形式になっています。XMLのタグはXMLコードの中で使用し、<para/> という形式です。あいにく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では、解析ツリー (XMLではなくJavaの解析ツリーであることに注意してください) を格納するためのクラスが数多く定義されています。その中でHCにとって最も重要なものは、RootDoc、ClassDoc、およびMethodDoc です。これらのクラスは、それぞれ、解析ツリー、クラス、およびメソッドについての情報を返します。
HCコンパイラーはCompilerDoclet です。このクラスは、@xmlns タグから名前空間の宣言を、また@xpath タグからメソッドに結び付けられたXPathを収集し、HCTablesGenerator (後で紹介します) を使ってテーブル・クラスを作成します。
CompilerDoclet の完全なリストは、オンラインで入手できます (参考文献を参照)。リスト1はstart() メソッドです。このメソッドは、コマンド行引数を抽出し、解析ツリーを処理します。HCエラーを正しく報告するために内部クラスDocletMessenger を使用していることに注意してください。
リスト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 という2つのクラスが定義されています。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);
}
|
RootDoc とMethodDoc に対しては、別のcompile() メソッドがあります。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のリフレクションはあまり役に立たないので、意図的に使わないようにしています。コードをたどってみるとわかるように、コンパイラーは非常に柔軟にこれらのメソッドを作成します。たとえば、ユーザーはパラメーターをかなり自由に制御できます。これは、リフレクションを使った実装では禁止されていることですが、この方法ではまったく問題ありません。
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に関する問題
このアプリケーションは、なぜ、XPathと同数のDFAを作成するのかと不思議に思うかもしれません。DFAを1つだけにするよりかえって効率が悪くなります。このようにしたのは、予期しない問題の発生に対して、これが最善のトレードオフであると考えられるからです。
このコラムでの約束事は、プロジェクトでの作業を包み隠さず紹介するということです。私は、どのような方法で問題の解決を試み、その過程でどのようなことがわかったのかを、読者の皆さんと共有することを約束しました。むしろ、問題が発生し、それによって読者の皆さんが同じ状況を回避する際の参考になることを期待しています。
今回の問題は、私のちょっとした誤解によるものです。前回のコラムでは、OR演算子を使ってXPathを結び付け、1つのDFAを作成していました。つまり、次のようなことです。
simpara/ulink sect1info/title |
上の2つのXPathを、次のように記述されているものとして扱っていました。
simpara/ulink | sect1info/title |
これは別に問題がないように見え、ほとんどの場合には実際にそのとおりです。しかし、次のような場合にはうまくいきません。前のXPathの最後が次のXPathの先頭と同じ場合は、正しくないのです。たとえば、次のような2つのXPathがあるものとします。
sect1/simpara simpara/ulink |
上の記述は、次の記述と同じではありません。
sect1/simpara | simpara/ulink |
違いがわかるでしょうか。私はしばらくわかりませんでした。この場合の問題は、DFAはsect1/simpara を認識すると、他のブランチ (simpara/ulink) を探索しないのです。上の2つのXPathは、実際には次の記述と等しくなります。
sect1/(simpara|simpara/ulink) | simpara/ulink |
これは正しいXPath構文ではありませんが、括弧が異なる優先順位を暗黙に指定しています。
何が間違っていたのでしょう。アルゴリズムを探し始めたとき、私は特定のコンテキスト (正規表現) の例を採り上げて、それをまったく異なるコンテキストに対応させようとしました。問題をいくつか発見しましたが (XPathに対するシンボル空間が無制限であることなど)、この問題は見落としていました。
私は、コラムを何回かかけてアルゴリズムを修正するのか、それとも満足度は低くても (複数のDFAを並行して実行すること) 安定した技術的ソリューションでリリースするのかを、選択する必要がありました。XMのときと同じように、動作するバージョンをこのコラムで提供するための締め切りを自ら設け、プロジェクトをいったん中止し、実践的な経験を得ることにします。
技術者としては、もっとエレガントな技術的ソリューションを見つけるために、締め切りなど無視したいところです。一方、コンサルタントとしては、少しくらい動作が遅くても安定した製品を早くリリースするのが最善であることを学んできました。実際の経験に勝るものはなく、2回目のリリースはパフォーマンスを最適化する最善の機会です。
HCの使い方
HCを採り上げたこのシリーズの締めくくりとして、簡単なハウツー・ガイドを紹介しておきましょう。リスト7 に示すHCアプリケーション・ハンドラーは、HTML内のDocbookの小さなサブセットのフォーマットを設定するものです。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.*
|
パラメーターを1つずつ確認しておきます。-docletpath はDocletに対するクラスパスです。-doclet によりDocletが選択されます。-classpath はアプリケーション・ハンドラーに対するクラスパスです (混乱しないでください)。そして、-d は出力ディレクトリーです。
最後のパラメーター (org.ananas.hc.test.*) では、使用するパッケージを選択します。
javacまたはjikesでアプリケーション (クラス・ファイルを含みます) をコンパイルして実行します。以上で準備は万全です。
次回の予定
最初にお話ししたように、実地の経験を得るために、HCの開発はいったん中止するつもりです。完全には程遠い状態ですが、このコラムで繰り返し述べたように、ソフトウェアの実践的なテストを行うことで、改善の必要な個所が明らかになるものと信じています。
ぜひ試してみてください。HCをダウンロードしてテストし、お気付きの点をananas-discussionメーリング・リストにお知らせください。
次回のコラムでは、「実用的なXML」での3番目のプロジェクトを開始します。これまでと同じように、新しいプロジェクトもオープン・ソースとしてリリースする予定です。
参考文献
- このプロジェクトに関連するコードは、ananas.org からダウンロードできます。そこからdeveloperWorks のCVSリポジトリー、さらにananas-discussionメーリング・リストにリンクをたどってください。ぜひメーリング・リストに参加し、皆様の知恵をプロジェクトに貸してくださることをお願いします。
- ご希望の場合は、ZIPファイルも入手可能です。
-
Jikes は、IBMから提供されている優れたJavaコンパイラーで、ビルド時間を大幅に短縮してくれます。
- Robert BerlinskiのSIA Parser と、このプロジェクトを比較してみてください。
- SAXパーサーを使ってJavaクラス内のXMLスキーマをコンパイルするJaxMe フレームワークも見てください。
著者について  | 
|  | Benoit Marchal氏は、ベルギーのナミュールを拠点にしたコンサルタントおよび著述家です。彼の著作には、 XML by Example(Que社、邦訳: インプレス社「実例で学ぶXML」。間もなく第2版が出版される予定です)、 Applied XML Solutions および XML and the Enterprise があります。また、Gamelanのコラムや、developerWorks XML zoneのコラムWorking XML の著者でもあります。最新プロジェクトの詳細については、www.marchal.com をご覧ください。 |
記事の評価
|