強力なAPI、SAX

このXML by Exampleのプレビューでは、DOMとSAXを比較し、SAXの使用法を説明します

今回の記事は、Benoit Marchal著のXML by Example 第2版のプレビューであり、SAXへの心強い入門記事です。SAXは、XMLを処理するための、イベント・ベースのAPIであって、いまや事実上の標準になっています。このプレビューでは、どのような場合に、DOMに代えてSAXを使用するかを示し、また、一般に用いられる SAXインターフェースの概要を述べます。さらに、Javaをベースとするアプリケーションを用いて詳しい例を、多くのコード・サンプルと共に提供します。これらは、Pearson Technology Groupの1部門であるQue Publishingの許可を得て使用しています。

Benoit Marchal (bmarchal@pineapplesoft.com), Consultant, Pineapplesoft

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



2001年 8月 01日

本稿では、出版予定のXML by Example 第2版から1つの章に手を加え、SAXの入門記事 としました。SAXは、XMLを処理するためのイベント・ベースAPIであり、Document Object ModelすなわちDOMを補完するものです。DOMは、W3Cにより公開されたXMLパーサー用のオブジェクト・ベースAPIです。

SAXについては、次のようなことが言えます。

  • イベント・ベースのAPIであること。
  • DOMより下位のレベルで動作すること。
  • DOMより細かい制御ができること。
  • DOMより常に効率が良いこと。
  • しかし残念ながらDOMより手が掛かること。

なぜ、別のAPIが必要なのか?

名前に惑わされないでください。SAXは、Simple API for XML (XML用の簡素なAPI) の略ですが、DOM以上の作業が必要となります。その代わり、努力するだけの価値のある、堅固なコードが得られます。

図1は、典型的なXMLプログラムの2つのコンポーネントを示しています。

  • パーサーは、アプリケーションに代わって、XMLファイルをデコードするソフトウェア・コンポーネントです。パーサーは、XML構文の複雑さから開発者を効果的に保護します。
  • アプリケーションは、ファイルの内容を使います。
図1. XMLプログラムのアーキテクチャー
XMLアプリケーション・アーキテクチャーのフローチャート

言うまでもなく、アプリケーションは、ユーロとドルの間で通貨を換算するアプリケーションのように単純な場合も、インターネット上で商品を注文する分散型のe-commerceアプリケーションのように極めて複雑な場合もあります。

この章では、図1の点線、パーサーとアプリケーションとの間のインターフェース、すなわちAPI (アプリケーション・プログラミング・インターフェース) に焦点を当てます。


オブジェクト・ベースおよびイベント・ベースのインターフェース

ご存じのとおり、パーサー用インターフェースには2つの分類、すなわち、オブジェクト・ベースのインターフェースとイベント・ベースのインターフェースがあります。

DOMは、オブジェクト・ベースのパーサー用の標準APIであり、W3Cにより開発、公開されました。これについては、私の著作の他の章で細かく説明しています。ここでDOMの概要は、SAXが全体の中でどのように位置付けられているかを理解するのに役立ちます。

DOMは、オブジェクト・ベースのインターフェースとして、メモリー上にオブジェクトのツリーを明示的に構築することにより、アプリケーションと交信します。オブジェクトのツリーは、XMLファイルのエレメントのツリーを正確にマップしたものです。

DOMは、基礎となるXML文書に忠実に適合しているので、これを習得し、使用するのは簡単です。また、ブラウザーやエディターなど、私がXML中心のアプリケーションと呼んでいるものに対しても理想的です。XML中心のアプリケーションとは、XML文書を処理すること自体が目的でXML文書を処理するアプリケーションを言います。

しかし、ほとんどのアプリケーションにとって、XML文書の処理は、他の多くの作業の中の1つに過ぎません。たとえば、会計パッケージは、XMLの送り状をインポートしますが、それが主要な処理ではありません。勘定の決済、支出の追跡、送り状に対する支払いの照合などが主要な処理になります。会計パッケージの多くは、データベースのようなデータ構造を既に持っている可能性があります。DOMモデルは、このような会計アプリケーションには適していません。なぜなら、アプリケーションはメモリー上にデータのコピーを2つ (1つはDOMツリー内に、もう1つはアプリケーション自身の構造内に) 持たなければならないからです。少なくとも、メモリーにデータを二重に保持するのは、非効率的です。デスクトップ・アプリケーションには重要な問題にはならなくても、サーバーにとっては弱点となり得ます。

SAXは、「XML中心のアプリケーション」以外のアプリケーションにとって適切な選択肢です。実際、SAXは、文書ツリーをメモリー上に明示的には構築しません。SAXを用いればアプリケーションは、最も効率的な方法でデータを保管できるようになります。

図2では、アプリケーションがXMLツリーとそのデータ構造との間のマップをどのように実行するかを示しています。

図2. XML構造をアプリケーション構造にマップする
図は、XML構造をアプリケーション構造にマップする方法を示します

イベントをベースとするインターフェース

その名の示すとおり、イベント・ベースのパーサーは、アプリケーションにイベントを送ります。イベントは、 ONCLICK イベント (ブラウザー) またはAWT/Swingイベント(Java) などのユーザー・インターフェース・イベントに似ています。

またイベントは、何かが起こりアプリケーションが応答する必要があることを、アプリケーションに知らせます。ブラウザーでは、通常、イベントはユーザーのアクションに応じて生成されます。ユーザーがボタンをクリックすると、そのボタンは、 ONCLICK イベントを発行します。

XMLパーサーにより、イベントは、ユーザー・アクションにではなく、読み込まれるXML文書のエレメントに関連付けられます。以下に対するイベントがあります。

  • エレメントの開始タグおよび終了タグ
  • エレメントの内容
  • エンティティー
  • 構文解析におけるエラー

図3では、パーサーが文書を読み込む際、イベントを生成する様子を示しています。

図3. パーサーがイベントを生成する
図では、パーサーがイベントを生成する様子を示しています。

リスト1は、XMLで書かれた価格表を示します。ここでは、様々な企業により請求されたXMLの講習会の価格が詳しく記述されています。図4は、価格表文書の構造を示します。

リスト1. pricelist.xml
<?xml version="1.0"?>
<xbe:price-list xmlns:xbe="http://www.psol.com/xbe2/listing8.1">
   <xbe:product>XML Training</xbe:product>
   <xbe:price-quote price="999.00" vendor="Playfield Training"/>
   <xbe:price-quote price="699.00" vendor="XMLi"/>
   <xbe:price-quote price="799.00" vendor="WriteIT"/>
   <xbe:price-quote price="1999.00" vendor="Emailaholic"/>
</xbe:price-list>
図4. 価格表の構造
価格表の構造を表す図

XMLパーサーは、この文書を読み込んで解釈します。パーサーは、文書内で何かを認識するたびに、イベントを生成します。

リスト1 を読む際、パーサーは、まず、XML宣言部を読み込み、文書の開始のためのイベントを生成します。パーサーが最初の開始タグ、 <xbe:price-list> に出合うと、2番目のイベントを生成し、 price-list エレメントの開始タグに出合ったことをアプリケーションに通知します。

次に、パーサーは、 product エレメントの開始タグを見て (分かりやすくするため、ここから先はネームスペースと、空白インデントを無視します)、3番目のイベントを生成します。

開始タグの後で、パーサーは product エレメントである XML Training の内容を見ます。これによりもう1つのイベントが生成されます。

次のイベントは、 product エレメントの終了タグを示します。パーサーは product エレメントの構文解析を完了しました。ここまでで、5つのイベントが発行されています。 product エレメントに3つ、文書の開始に1つ、 price-list の 開始タグに1つです。

ここでパーサーは、最初の price-quote エレメントに移ります。各 price-quote エレメントに2つのイベントを生成します。1つは開始タグ、もう1つは終了タグのイベントです。

お分かりのとおり、終了タグが、開始タグ内の / 文字に省略されていても、パーサーは、終了イベントを生成します。

price-quote エレメントが4つあるので、パーサーは、それらを解析してイベントを8つ生成します。最後にパーサーは、 price-list の終了タグに出合い、最後の2つのイベントを生成します。つまり price-list の終了のイベントと文書の終わりのイベントです。

図5で示しているように、総合すると、イベントはドキュメント・ツリーをアプリケーションに提示します。開始タグのイベントは、「ツリーで1つ段階を下げる」ことを意味するのに対し、終了タグのエレメントは、「ツリーの段階を1つ上げる」ことを意味します。

図5. パーサーがツリーを暗黙的に構築する方法
図は、SAXパーサーがDOMツリーを暗黙的に構築する様子を示します。

パーサーは、XML文書のドキュメント・ツリーを構築するのに充分な情報を渡しますが、DOMパーサーとは異なり、明示的にツリーを構築しないことに注目してください。

なぜ、イベント・ベース・インターフェースを使用するのか?

ここまで読んで、読者は混乱しているものと思います。SAXとDOMのどちらのタイプのAPIを使用すべきなのでしょうか、またどのような場合にこれらを使用すべきなのでしょうか。残念ながら、この疑問に明白な答えはありません。2つのAPIのいずれも本質的にどちらが優れているとは言えません。ニーズによって異るからです。

経験則から言えば、細かい制御を行いたい場合にはSAXを使用し、利便性が重要なときにはDOMを使用するのが良いでしょう。たとえば、DOMは、スクリプト言語でよく利用されます。

注: 本来の役割

イベント・ベース・インターフェースは、パーサーのインターフェースとして最も自然なものです。このインターフェースは、見たものを単に報告するだけです。

SAXを採用する主な理由は、効率が良いからです。SAXは、DOMと比較してできることは少ないのですが、構文解析する際には、より細かい制御が可能です。無論パーサーの能力が低ければ、開発者の作業は増えます。

また、既に述べたように、SAXは、ドキュメント・ツリーを構築する必要がないので、DOMほどリソースを消費しません。

XMLが登場した初期には、DOMは、W3Cの認めた公式APIであるという利点がありました。開発者は、利便性より性能を求めて、次第にSAXへ移っています。

SAXにおける主な制限は、文書内で逆方向のナビゲートができないことです。実際、イベントを出した後、パーサーはそのことを忘れてしまいます。お分かりのように、アプリケーションは、関連するイベントを明示的にバッファーしなければなりません。

注: SAXが構築するツリー

必要なら、アプリケーションは、パーサーから受け取るイベントからDOMツリーを構築できます。事実、いくつかのDOMパーサーは、SAXパーサーの上に構築されます。

もちろん、SAXとDOM APIのどちらをインプリメントしていようと、パーサーは、多くの役立つ処理を行います。パーサーは、文書を読み込み、XML構文を行い、エンティティーを解決するなど、多くのことをします。また、妥当性を検証パーサーは、文書スキーマを適用します。

パーサーを利用する理由は数多くあり、皆さんもSAX、DOMの両方のAPIを習得することをお勧めします。これにより、現在抱えている作業に必要な最善のAPIを選択できる柔軟性が得られます。幸い、最近のパーサーは、両方のAPIをサポートしています。


強力なAPI、SAX

SAXは、イベント・ベースのパーサーに向けた標準的で簡素なAPIとして、XML-DEVメーリング・リストのメンバーにより開発されました。SAXは、Simple API for XMLの略です。

SAXは、元来Java向けに定義されたものですが、Python、Perl、C++ およびCOM(Windowsオブジェクト) でも利用できます。また、さらに多くの言語で利用できるようになるでしょう。さらに、SAXパーサーは、COMを通じてVisual BasicやDelphiなどWindows用プログラミング言語でも利用できます。

SAXは、DOMとは異なり、公式の標準化団体が認めたものではありませんが、広範囲に使用され、事実上の標準と見なされています。(現在SAXは、David Megginson氏が監修していますが、同氏は将来そのポストから退く意向を表明しています。)

これまで見てきた通り、DOMは、ブラウザーでは、好まれるAPIです。したがって、この章における例は、Javaで記述されています。(ここで読者が、Javaの速成学習コースを希望するなら、私の著作の付録Aまたは、DeveloperWorks、Java zoneのEducationのセクションをご覧ください。)

SAXをサポートするパーサーには、ApacheのパーサーであるXerces (以前はIBMパーサーと呼ばれていました)、MicrosoftのパーサーであるMSXML、Oracleのパーサー、XDKなどがあります。これらのパーサーは、DOMもサポートしているので非常に柔軟性に富んでいます。

SAXだけを提供するパーサーは少なく、James Clark氏のXP、AElig;lfred、Vivid Creations (参考文献 の項をご覧ください) のActiveSAXがあります。


SAX入門

リスト2 は、リスト1で最も低価格のオファーを検索するJavaアプリケーションです。このアプリケーションは、最低価格およびそのベンダー名を印刷します。

例のコンパイル

このアプリケーションをコンパイルするには、ご使用のプラットフォームに対応するJava DevelopmentKit (JDK) が必要です ( 参考文献 の項をご覧ください)。この例ではJavaランタイムだけでは不十分です。

注意

Javaは空白を含むパスを、苦手とします。Cheapestがファイルを見つけ出せないという場合は、不正な空白を含むディレクトリーがないか調べてください。

著者のWebサイト のXBE2ページから本記事のリストをダウンロードしてください。ダウンロード・ファイルの中に、Xercesがあります。このリストに問題がある場合、著者のWebサイトを訪れ、最新版を入手してください。

Cheapest.java ファイルの リスト2 を保管してください。DOSプロンプトで、 Cheapest.java を保管したディレクトリーに移動し、DOSプロンプトで以下のコマンドを発行し、コンパイルます。

mkdir classes
set classpath=classes;lib\xerces.jar
javac -d classes src\Cheapest.java

コンパイルの結果、classディレクトリーにJavaプログラムがインストールされます。これらのコマンドは、Xercesが lib ディレクトリーに、リスト2が src ディレクトリーにあると想定しています。異なるディレクトリーに、パーサーをインストールしてある場合、 classpath (2番目のコマンド) を変更する必要があります。

price listに対するアプリケーションを実行するには、以下のコマンドを発行します。

java com.psol.xbe2.Cheapest data\pricelist.xml

結果は、次のようになるはずです。

The cheapest offer is from XMLi ($699.00)

ここでは、data\pricelist.xmlのファイルに リスト1 があると想定しています。ここでも、ご自分のシステムのパスに適合するよう修正する必要があります。

ヒント: イベント・ハンドラーについて

イベント・ハンドラーは、パーサーを呼び出しません。実際は、逆にパーサーがイベント・ハンドラーを呼び出します。混乱しましたか? AWTイベントを思い出してください。ボタンに付属するイベント・ハンドラーは、ボタンを呼び出しません。ボタンがクリックされるのを待ちます。

イベント・ハンドラーのステップ・バイ・ステップ

SAXのイベントは、特定のJavaインターフェースに接続するメソッドとして定義されます。このセクションでは、リスト2をステップ・バイ・ステップに検討していきます。以下のセクションでは、SAXの主なインターフェースの情報について説明します。

イベント・ハンドラーを宣言するのに最も簡単な方法は、SAXが提供する DefaultHandler を継承することです。

public class Cheapest 
extends DefaultHandler

このアプリケーションは、イベント・ハンドラー startElement() 1つだけをインプリメントします。パーサーは、開始タグに出合ったとき、このイベント・ハンドラーを呼び出します。パーサーは、次の文書のすべての開始タグについて startElement() を呼び出します。すなわち、 <xbe:price-list><xbe:product> および <xbe:price-quote> です。

リスト2では、イベント・ハンドラーは、 price-quote だけに関係するので、これを検査します。ハンドラーは、他のエレメントのイベントに関しては何もしません。

if(uri.equals(NAMESPACE_URI) && name.equals("price-quote"))
{
   // ...
}

イベント・ハンドラーは、 price-quote エレメントを見付けると、属性のリストからベンダー名と価格を抽出します。この情報を武器にして、最低価格を探すのは簡単です。

String attribute =
   attributes.getValue("","price");
if(null != attribute)
{
   double price = toDouble(attribute);
   if(min > price)
   {
      min = price;
      vendor = attributes.getValue("","vendor");
   }
}

イベント・ハンドラーは、エレメント名、ネームスペースおよび属性リストを、パーサーからパラメーターとして受け取ることに注目してください。

ここで、 main() メソッドに目を向けましょう。このメソッドは、イベント・ハンドラー・オブジェクトとパーサー・オブジェクトを作成します。

Cheapest cheapest = new Cheapest();
XMLReader parser =
   XMLReaderFactory.createXMLReader(PARSER_NAME);

XMLReader および XMLReaderFactory は、SAXによって定義されます。 XMLReader は、SAXパーサーです。factoryは、 XMLReaders を作成するためのヘルパー・クラスです。

main() は、パーサーのフィーチャーがネームスペースの処理を要求するよう設定し、イベント・ハンドラーをパーサーに登録します。最後に、main() は、parse()メソッドを、XMLファイルへのURIを使用して呼び出します。

parser.setFeature("http://xml.org/sax/features/namespaces",true);
parser.setContentHandler(cheapest);
parser.parse(args[0]);

ヒント: ネームスペース

http://xml.org/sax/features/namespacesは、デフォルトでは、trueに設定されていますが、これを明示的にtrueに設定するとコードがさらに読みやすくなります。

単純に見える parse() メソッドは、XML文書の解析をトリガーします。XML文書は、次にイベント・ハンドラーを呼び出します。 startElement() メソッドが呼び出されるのは、このメソッドの実行中です。 parse() を呼び出す陰では、様々なことが行われています。

最後になりましたが重要なのは、 main() が、結果を印刷することです。

Object[] objects = new Object[]
{
   cheapest.vendor,
   new Double(cheapest.min)
};
System.out.println(MessageFormat.format(MESSAGE,objects));

ここで疑問が生じます。 Cheapest.vendorCheapest.min は、その値を、いつ取得するのでしょう? ここでは、これらの値を main() に明示的に設定していません。そうです。これはイベント・ハンドラーの仕事です。イベント・ハンドラーは、最終的には parse() により呼び出されます。ここが、イベント処理の優れた点です。

注意

Java Development Kitがインストールされていないと、この例がコンパイルできないことを思い出してください。。

最後に、

src\Cheapest.java:7: Package org.xml.sax
  not found in import.
import org.xml.sax.*;

または

Can't find class com/psol/xbe2/Cheapest
   or something it requires

のようなエラーが出された場合、原因は、以下のうちの1つであることがほとんどです。

  • classpath (2番目のコマンド、 classes;lib\xerces.jar) が間違っている。
  • 最後のコマンド (com.psol.xbe2.Cheapest) に誤ったクラス名が入力された。

一般的に使用されるSAXインターフェースおよびクラス

ここまで、1つのイベント ( startElement() ) だけを見てきました。先に進む前に、SAXで定義される主なインターフェースについて検討しましょう。

注: SAXのバージョン

SAXには、SAX1とSAX2の2つのバージョンがあります。この章で説明するのは、SAX2 APIだけです。SAX1は、SAX2と良く似ていますが、ネームスペースの処理ができません。

SAXは、イベントを複数のインターフェースに分類しています。

  • ContentHandler は、文書自身 (開始タグまたは終了タグなど) に関連するイベントを定義します。ほとんどのアプリケーションが、これらのイベントに対して登録を行います。
  • DTDHandler は、DTDに関連するイベントを定義します。しかし、DTDに関して完全な情報をレポートできるだけのイベントは定義しません。DTDを解析する必要がある場合には、オプションのDeclHandlerを使用してください。DeclHandlerは、SAXの拡張で、これをサポートしないパーサーもあります。
  • EntityResolver は、エンティティーのロードに関連するイベントを定義します。このイベントに対する登録を行うアプリケーションは、ほとんどありません。
  • ErrorHandler は、エラー・イベントを定義します。アプリケーションの多くは、独自の方法でエラーを報告するためにこれらのイベントに対する登録を行います。

注: SAXの最大の利点

このセクションは、SAXの総合的な解説ではありません。代わりに、最も良く使用されるクラスについて解説します。

作業を容易にするため、SAXでは、 DefaultHandler クラスにおいてこれらのインターフェースに対するデフォルト・インプリメンテーションを提供しています。ほとんどの場合、インターフェースを直接インプリメントせずに、 DefaultHandler を拡張して、アプリケーションに関連するメソッドをオーバーライドするほうが容易です。

XMLReader

イベント・ハンドラーを登録し解析を開始するために、アプリケーションは、 XMLReader インターフェースを使用します。すでに説明したように、 XMLReader のメソッド、 parse() が解析を始めます。

parser.parse(args[0]);

XMLReaderの主なメソッドには、以下ものがあります。

  • parse() は、XML文書を解析します。 parse() には、2つのバージョンがあります。1つは、ファイル名またはURLを受け取ります。もう1つは、オブジェクト InputSource (「InputSource」の項を参照) を受け取ります。
  • setContentHandler()setDTDHandler()setEntityResolver() および setErrorHandler() を用いて、アプリケーションは、イベント・ハンドラーを登録します。
  • setFeature() および setProperty() は、パーサーの動作を制御します。これらは、プロパティーまたはフィーチャー識別子と値を取得します。この識別子は、ネームスペースに似たURIです。プロパティーは、オブジェクトを取るのに対し、フィーチャーは、ブール値を取ります。

XMLReaderFactoryで最も一般的に使用されるフィーチャーは、以下のとおりです。

  • http:// xml.org/sax/features/namespaces 。これは、すべてのSAXパーサーが、認識します。trueに設定すると (デフォルト)、 ContentHandler のメソッドを呼び出す際に、パーサーは、ネームスペースを認識し、接頭部を解決します。
  • http://xml.org/sax/features/validation 。これは、オプションです。trueに設定されていると、検証パーサーが、文書を検証します。非検証パーサーは、このフィーチャーを無視します。

XMLReaderFactory

XMLReaderFactory は、パーサー・オブジェクトを生成します。これは、2つのバージョンの createXMLReader() を定義します。最初のバージョンは、パラメーターとしてパーサーに対するクラス名を取得します。2番目のバージョンは、 org.xml.sax.driver システム・プロパティーからクラス名を取得します。

Xercesでは、クラスは、 org.apache.xerces.parsers.SAXParser です。 XMLReaderFactory を使用すると、他のパーサーに容易に変換できるので、これを使用すべきです。実際、ここで必要なのは、1行変更して再コンパイルすることだけです。

XMLReader parser = XMLReaderFactory.createXMLReader(
				"org.apache.xerces.parsers.SAXParser");

さらなる柔軟性を持たせるため、アプリケーションは、コマンド・ラインからクラス名を読み込むことも、パラメーター無しの createXMLReader() を使用することもできます。したがって、再コンパイルせずにパーサーを変更できます。

InputSource

InputSource は、パーサーが ファイルを読む方法を制御します。ファイルには、XML文書とエンティティーが含まれます。

ほとんどの場合、文書はURLからロードされます。しかし、特殊なニーズを持つアプリケーションは、 InputSource をオーバーライドできます。たとえば、データベースから文書をロードする場合に使用されます。

ContentHandler

ContentHandler は、XML文書のイベントを定義するので、使用されることが最も多いSAXインターフェースです。

ここまで見てきたように、 リスト2 は、 ContentHandler 内で定義されるイベント、 startElement() をインプリメントします。これは、パーサーに ContentHandler を登録します。

Cheapest cheapest = new Cheapest();
// ...
parser.setContentHandler(cheapest);

ContentHandler は、以下のイベントを宣言します。

  • startDocument() / endDocument() は、アプリケーションに文書の先頭または末尾を通知します。
  • startElement() / endElement() は、アプリケーションに開始タグまたは終了タグを通知します。属性は、 Attributes パラメーターとして渡されます (次の属性のセクションを参照)。空エレメント (たとえば <img href="logo.gif"/> ) は、たとえ タグが1つしかなくても startElement() および endElement() の両方を生成します。
  • startPrefixMapping() / endPrefixMapping() は、アプリケーションにネームスペースの範囲を通知します。 http://xml.org/sax/features/namespaces がtrueである場合、パーサーは、既にネームスペースを解決しているので、この情報を必要とすることはほとんどありません。
  • characters() / ignorableWhitespace() は、エレメント内の最初のテキスト (解析済みの文字データ) を見つけると、アプリケーションにそのことを通知します。パーサーは、いくつものイベント間でテキストを展開することができます (そのバッファーを効率よく管理するため)。 ignorableWhitespace イベントは、XML標準により定義される、無視できるスペースに対して使用できます。
  • processingInstruction() は、処理命令をアプリケーションに通知します。
  • skippedEntity() は、アプリケーションにエンティティーがスキップされたこと(すなわち、パーサーが、DTD/スキーマのエンティティー宣言を認識しなかったとき) を通知します。
  • setDocumentLocator() は、 Locator オブジェクトをアプリケーションに渡します。後述のLocatorのセクションを参照してください。SAXパーサーは、必ずしも Locator を持つ必要はありませんが、持っている場合、他のいかなるイベントにも先立ってこのイベントを起動しなければなりません。

属性
startElement() イベント内で、アプリケーションは、 Attributes パラメーター内の属性のリストを受け取ります。

String attribute = attributes.getValue("","price");

Attributes は、以下のメソッドを定義します。

  • getValue(i) / getValue(qName) /getValue(uri,localName) は、i番目の属性の値、または名前が指定されている属性の値を戻します。
  • getLength() は、属性の数を戻します。
  • getQName(i) / getLocalName(i) /getURI(i) は、i番目の属性の修飾名 (接頭部を持つ)、ローカル名 (接頭部を持たない) およびネームスペースURIを戻します。
  • getType(i)/getType(qName)/getType(uri,localName) は、 i 番目の属性の型または、名前が指定されている属性の型を戻します。型は、DTDで使用されるような文字です。これには "CDATA""ID""IDREF""IDREFS""NMTOKEN""NMTOKENS""ENTITY""ENTITIES" または "NOTATION" などがあります。.

注意

Attributes パラメーターは、 startElement() イベント中に限り使用できます。これがイベントとイベントとの間で必要な場合には、 AttributesImpl を用いてコピーしてください。

Locator

Locator によって、アプリケーションは行と列を使用できます。ただしパーサーは、 Locator オブジェクトを提供する必要はありません。

Locator は、以下のメソッドを定義します。

  • getColumnNumber() は、現在のイベントが終了した列を戻します。 endElement() イベントでは、終了タグの最後の列を戻します。
  • getLineNumber() は、現在のイベントが終了した行を戻します。 endElement() イベントでは、終了タグの行を戻します。
  • getPublicId() は、現在のdocumentイベントの公開識別子を戻します。
  • getSystemId() は、現在のdocumentイベントのシステム識別子を戻します。

DTDHandler

DTDHandler は、DTDの解析に関連する2つのイベントを宣言します。

  • notationDecl() は、アプリケーションに、表記法が宣言されたことを通知します。
  • nparsedEntityDecl() は、アプリケーションに未解析のエンティティー宣言が検出されたことを通知します。

EntityResolver

EntityResolver インターフェースは、1つのイベント、すなわち、 resolveEntity() だけを定義します。これは、 InputSource (別の章に説明があります) を戻します。

SAXパーサーは、ほとんどのURLを解決できるので、 EntityResolver をインプリメントするアプリケーションは少ししかありません。例外は、カタログ・ファイル(別の章に説明があります) です。これは、公開識別子をシステム識別子に変換します。アプリケーションがカタログ・ファイルを必要とする場合は、Norman Walsh氏のカタログ・パッケージをダウンロードしてください ( 参考文献 を参照)。

ErrorHandler

ErrorHandler インターフェースは、エラーに関するイベントを定義します。これらのイベントを扱うアプリケーションは、独自のエラー処理を行うことができます。

独自のエラー・ハンドラーをインストールすると、パーサーは例外を生成しなくなります。例外をスローするのは、イベント・ハンドラーの役割になります。

このインターフェースは、エラーの3のレベル、すなわち重大度に対応する3つのメソッドを定義します。

  • warning() は、XMLの仕様で定義されているエラー以外の問題を知らせます。たとえば、パーサーによっては、XML宣言がない場合に警告を発行するものがあります。これはエラーではないが (宣言はオプションなので)、注意を払う価値はあります。
  • error() は、XMLの仕様により定義されているエラーがあることを知らせます。
  • fatalError() は、XMLの仕様により定義される致命的エラーを知らせます。

SAXException

SAXにより定義されるほとんどのメソッドは、 SAXException をスローできます。 SAXException は、XML文書を解析している間エラーを知らせます。

エラーは、解析エラーまたはイベント・ハンドラーのエラーかのいずれかです。イベント・ハンドラーから、その他の例外を報告するには、 SAXException の例外をラップすることによって可能になります。

例:startElement イベントを処理している間に、イベント・ハンドラーが IndexOutOfBoundsException をキャッチしたと仮定してください。イベント・ハンドラーは、 SAXException 内で IndexOutOfBoundsException をラップできます。

public void startElement(String uri,
                         String name,
                         String qualifiedName,
                         Attributes attributes)
{
   try
   {
      // the code may throw an IndexOutOfBoundsException
   }
   catch(IndexOutOfBounds e)
   {
      throw new SAXException(e);
   }
}

SAXException は常に、それがキャッチされ、中断された parse() メソッドへ向かって流れます。

try
{
   parser.parse(uri);
}
catch(SAXException e)
{
   Exception x = e.getException();
   if(null != x)
      if(x instanceof IndexOutOfBoundsException)
         // process the IndexOutOfBoundsException
}

状態の保守

リスト1 は、情報がpriceエレメントの属性として保管されるので、SAXパーサーには便利です。アプリケーションは、 startElement() に対してのみ登録しなければなりません。

リスト3は、情報がいくつものエレメント間に分散しているので、さらに複雑です。特に、ベンダーは、配送の時期により異なる価格を持っています。ユーザーが待つことをいとわなければ、より適切な価格で購入できるかもしれません。図6は、文書の構造を示しています。

リスト3. xtpricelist.xml
<?xml version="1.0"?>
<xbe:price-list xmlns:xbe="http://www.psol.com/xbe2/listing8.3">
   <xbe:name>XML Training</xbe:name>
   <xbe:vendor>
      <xbe:name>Playfield Training</xbe:name>
      <xbe:price-quote delivery="5">999.00</xbe:price-quote>
      <xbe:price-quote delivery="15">899.00</xbe:price-quote>
   </xbe:vendor>
   <xbe:vendor>
      <xbe:name>XMLi</xbe:name>
      <xbe:price-quote delivery="3">2999.00</xbe:price-quote>
      <xbe:price-quote delivery="30">1499.00</xbe:price-quote>
      <xbe:price-quote delivery="45">699.00</xbe:price-quote>
   </xbe:vendor>
   <xbe:vendor>
      <xbe:name>WriteIT</xbe:name>
      <xbe:price-quote delivery="5">799.00</xbe:price-quote>
      <xbe:price-quote delivery="15">899.00</xbe:price-quote>
   </xbe:vendor>
   <xbe:vendor>
      <xbe:name>Emailaholic</xbe:name>
      <xbe:price-quote delivery="1">1999.00</xbe:price-quote>
   </xbe:vendor>
</xbe:price-list>
図6. Price listの構造
price listの構造

最も有利な取り引きを探すには、アプリケーションは、いくつものエレメントから情報を集めなければなりません。しかしながら、パーサーは、 startElement()characters() および endElement() の各エレメントごとに3つのイベントを生成できます。アプリケーションは、どうしてもイベントおよびエレメントを関連付ける必要があります。

リスト4 は、price listから最も有利な取り引きを探す新たな Javaアプリケーションです。最も有利な取り引きを探す際には、緊急度も考慮します。実際、リスト3で、最も低価格のベンダー (SMLi) の配送は最も遅くなっています。他方、Emailaholicは、高価ではありますが配送期間は2日です。

先に紹介したCheapestアプリケーションと同様、このアプリケーションをコンパイルし、実行します。結果は、緊急度によって変わります。このプログラムが2つのパラメーターを取るのが分かるでしょう。ファイル名と、待つことが可能な日数を表す最大遅延日数です。

java com.psol.xbe2.BestDeal data/xtpricelist.xml 60

は、以下を戻します。

The best deal is proposed by XMLi. A(n) XML Training delivered[ccc]
in 45 days for $699.00

ここで、

java com.psol.xbe2.BestDeal data/xtpricelist.xml 3

は、以下を戻します。

The best deal is proposed by Emailaholic. A(n) XML Training[ccc]
delivered in 1 days for $1,999.00

階層化アーキテクチャー

リスト4は、これまでの中で最も複雑なアプリケーションです。ただし、異常なものではありません。このSAXパーサーは、非常に下位レベルのパーサーなので、アプリケーションは、DOMパーサーを使用する場合よりも多くの作業を肩代わりしなければなりません。

アプリケーションは、2つのクラス、 SAX2BestDeal および BestDeal で編成されます。 SAX2BestDeal は、SAXパーサーとのインターフェースを管理します。これは、統一の取れた方法で状態を管理し、イベントをグループ化します。

BestDeal は、価格の比較を行うロジックを持っています。また、(XMLではなく) アプリケーション用に最適化された構造を持つ情報を保持します。このアプリケーションのアーキテクチャーを、図7に示します。図8は、UMLで記述したクラス図です。

図7. アプリケーションのアーキテクチャー
最良の取り引きアプリケーション・アーキテクチャーの図
図8. アプリケーションのクラス図
最良の取り引きアプリケーションのクラス図

SAX2BestDeal は、複数のイベント、 startElement()endElement() および characters() を扱います。 SAX2BestDeal は、文書ツリーの中の位置を常に追跡します。

たとえば、 characters() イベントにおいて、 SAX2BestDeal は、テキストが名前なのか、価格なのか、または無視できる空白なのか、を知る必要があります。さらに、2つの name エレメントがあります。すなわち、 price-listname および vendorname です。

状態

SAXパーサーは、DOMパーサーと異なり、状態に関する情報を提供しません。そのためアプリケーションは自身の状態を追跡する必要があります。これには、いくつものオプションがあります。リスト4では、状態およびその遷移の中で重要なものを識別しました。図6の文書構造からこれらの情報を取り出すのは難しいことではありません。

アプリケーションがprice-listタグに最初に出合うのは明らかです。したがって、最初の状態は price-list の中に あるはずです。アプリケーションは、そこから name に到達します。2番目の状態は、 price-listname です。

次のエレメントは、 vendor でなければならないので、3番目の状態は、 price-list 内の vendor の中に あります。nameはvendorの後にあるので、4番目の状態は、 price-list 内の vendor の中の name の中に あります。

name の後には、 price-quote エレメントが続き、対応する状態は、 price-list 内の vendor の中の price の中に あります。その後、パーサーは、すでに状態を持っている price-quote または vendor のいずれかに出合います。

この概念は、図9に見られるように状態と遷移を図上に視覚化したほうが分かりやすいでしょう。 price-list/name または price-list/vendor/name を処理するかによって2つの異なるnameエレメントに関連する2つの異なる状態があることに注意してください。

図9. 状態遷移図
状態遷移図

リスト4では、状態変数は、現在の状態を保管します。

final protected int START = 0,
                    PRICE_LIST = 1,
                    PRICE_LIST_NAME = 2,
                    VENDOR = 3,
                    VENDOR_NAME = 4,
                    VENDOR_PRICE_QUOTE = 5;
protected int state = START;

遷移

遷移1 状態変数の値は、イベントに応じて変化します。この例では、elementStart() が状態を更新します。

ifswitch(state)
{
   case START:
      if(name.equals("price-list"))
         state = PRICE_LIST;
      break;
   case PRICE_LIST:
      if(name.equals("name"))
         state = PRICE_LIST_NAME;
         // ...
      if(name.equals("vendor"))
         state = VENDOR;
         break;
   case VENDOR:
      if(name.equals("name"))
         state = VENDOR_NAME;
         // ...
      if(name.equals("price-quote"))
         state = VENDOR_PRICE_QUOTE;
         // ...
      break;
}

SAX2BestDeal は、現在の name および price-quote の内容を保管するためにインスタンス変数をいくつか持ちます。これは実際には、ツリーの小さなサブセットです。DOMとは異なり、ツリー全体を持つことは決してありません。アプリケーションが name および price-quote を使用し終わると、これらを破棄するからです。

これは、メモリー使用の観点から見ると非常に効率的です。実際、どの時点でもメモリーには小さなサブセットだけを配置するので、数ギガバイト単位のファイルを処理できるでしょう。

遷移2 パーサーは、文書の各文字データ (インデントを含む) に対して characters() を呼び出します。 name および price-quote のテキストだけを記録することは意味があるのでイベント・ハンドラーは、状態を使用します。

switch(state)
{
   case PRICE_LIST_NAME:
   case VENDOR_NAME:
   case VENDOR_PRICE_QUOTE:
      buffer.append(chars,start,length);
      break;
}

遷移3 endElement() のイベント・ハンドラーは、状態を更新し、現在のエレメントswitch(state) を処理するためにBestDealを呼び出します。

{
   case PRICE_LIST_NAME:
      if(name.equals("name"))
      {
         state = PRICE_LIST;
         setProductName(buffer.toString());
         // ...
      }
      break;
   case VENDOR_NAME:
      if(name.equals("name"))
         state = VENDOR;
         // ...
      break;
   case VENDOR_PRICE_QUOTE:
      if(name.equals("price-quote"))
      {
         state = VENDOR;
         // ...
         compare(vendorName,price,delivery);
         // ...
      }
      break;
   case VENDOR:
      if(name.equals("vendor"))
         state = PRICE_LIST;
         // ...
      break;
   case PRICE_LIST:
      if(name.equals("price-list"))
         state = START;
      break;
}

教訓

リスト4は、SAXアプリケーションとしては典型的なものです。これには、SAXイベント・ハンドラー ( SAX2BestDeal ) があります。これは、アプリケーションに最も適した形式でイベントをまとめます。

注: 状態なのかスタックなのか?

状態変数を使用する代わりに Stack を使用する方法もあります。 startElement() 内のname(または他の識別子) エレメントを スタックに入れ (push)、これを endElement() から取り出し (pop) ます。

アプリケーション・ロジック ( BestDeal 内の) は、イベント・ハンドラーから独立しています。実際、多くの場合、アプリケーション・ロジックは、XMLとは別個に記述されます。

階層化アプローチは、アプリケーションと解析とを明確に分離します。

また、この例では、SAXがDOMより効率的なことが明確に示されていますが、プログラマーの負担は増えます。特にプログラマーは、状態および状態間の遷移を明示的に管理しなければなりません。(DOMでは、ツリーを再帰的にたどるので状態は暗黙的です。)


柔軟性

XMLは、非常に柔軟性に富んだ標準です。しかし、実際には、XMLアプリケーションは、プログラマーが柔軟にするものです。このセクションでは、アプリケーションがXMLの柔軟性を確実に活用するヒントを提供します。

柔軟に構築する

BestDealアプリケーションは、XML文書の構造をほとんど制約しません。XML文書にエレメントを追加しても、それらは単に無視されます。たとえば、BestDealは、以下の vendor エレメントを受け入れます。

<xbe:vendor>
   <xbe:name>Playfield Training</xbe:name>
   <xbe:contact>John Doe</xbe:contact>
   <xbe:price-quote delivery="5">999.00</xbe:price-quote>
   <xbe:price-quote delivery="15">899.00</xbe:price-quote>
</xbe:vendor>

しかしcontact情報は無視されるでしょう。一般に、HTMLブラウザーがしているように、未知のエレメントを単に無視するというのは良い考えです。

構造を実施する

イベント・ハンドラーから文書構造を検証するのは難しいことではありません。以下の短いコード ( startElement() から適用しました) は、構造を検証し、vendorエレメントにnameまたはprice以外のものが格納されていると SAXException をスローします。

case VENDOR:
   if(name.equals("name"))
   {
      state = VENDOR_NAME;
      buffer = new StringBuffer();
   }
   else if(name.equals("price-quote"))
   {
      state = VENDOR_PRICE_QUOTE;
      String st = attributes.getValue("","delivery");
      delivery = Integer.parseInt(st);
      buffer = new StringBuffer();
   }
   else
      throw new SAXException("Expecting <xbe:name> or <xbe:price-quote>");
   break;

contact エレメントを伴うリストがあれば、ここには、以下のように記述されます。

org.xml.sax.SAXException: Expecting <xbe:name> or <xbe:price-quote>

しかしながら、実際には、アプリケーションが本当に文書構造に依存していれば、スキーマを記述し検証パーサーを使うのが最善です。


次に来るものは?

XML by Example 第2版から抜粋したこの記事では、XML文書の読み方を学習しました。この本の残りの部分では、文書の記述方法とループの閉じ方に関するプロセス全体の学習を行うことができます。この学習を完了するためオンライン・チュートリアルやオンライン記事を参照できます。(もちろん、私の著作を選択してくれることを望みますが。)

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Java technology
ArticleID=241331
ArticleTitle=強力なAPI、SAX
publish-date=08012001