XSLT
スタイルシートを作成した経験があれば、実際の環境で正確なスタイルシートを作成するのは必ずしも簡単ではないことをご存知でしょう。例えば、ほんの些細なタイプ・ミスが大きな問題を引き起こすこともあります。XPath
式に含まれる要素名や属性名の単純な誤りは、XSLT プロセッサーのエラー・チェック・メカニズムでは見過ごされてしまいます。/FileName としなければならないところを、/Filename としただけの単純なエラーは、XSLT エンジンではトラップされないため、独自のテストとデバッグ作業によって見つけるしかありません。
一例として、リスト 1 の XML 文書があるとします。
リスト 1. サンプル XML 文書
<Things>
<Thing thingid="12345FFD3">...</Thing>
<Thing thingid="86779EAD0">...</Thing>
...
</Things>
|
XSLT スタイルシートには、この Thing 要素に対する処理を行う名前付きテンプレートがあります (リスト 2 を参照)。
リスト 2. Thing 要素を処理する名前付きテンプレート
<xsl:template name="ProcessThing">
<xsl:param name="ThingId"/>
<xsl:for-each select="/Things/Thing[@id eq $ThingId]">
<!-- Do something with the thing -->
</xsl:for-each>
</xsl:template>
|
どこに間違いがあるかわかりますか?それは、テンプレートの作成者が、Thing 要素の ID 属性を thingid
という属性名にしたことを忘れて (あるいは知らずに)、id
という属性名として記述しているところです。このマークアップを実行しても、エラー・メッセージは表示されません。単に、for-each ループが実行されない結果となるだけです。このようなエラーに気付くかどうかはわかりませんが、このエラーが複雑な変換のどこかに紛れ込んだまま、それに気付かずにコードを本番環境に移したとすると、属性名の誤りによって計算の肝心な部分が実行されないことになります。
これに関連するもう 1 つの問題として、意図にまったく反して、入力文書のすべてがスキーマまたは DTD
に対して妥当性検証されるわけではないことが挙げられます。例えば、文書の作成者が <FileName> とするべきところを <Filename> とした場合を考えてみてください。
このようなエラー (少なくとも、最もあからさまなエラー) が自動的にトラップされて、問題がある場合には、XSLT プロセッサーがそれを通知してくれるのであれば安心です。この記事では、より正確な XSLT スタイルシートを作成し、通常は無視されてしまうエラーを XSLT プロセッサーにトラップさせる方法を探ります。そのなかで、スタイルシートをソフトウェア・エンジニアリングの観点から捉え、より堅牢かつセキュアなスタイルシートにする方法を説明します。記事では、読者が少なくとも実際的な XSLT の知識を持っていることを前提とします。
この記事で説明するメソッドのほとんどは、XSLT 2.0 の型システムについての知識がなければ理解できません (この記事では、XSLT 2.0 については大まかに紹介するだけなので、詳細については「参考文献」を参照してください)。XSLT 2.0 には、データ型だけでなく、多くの新機能が導入されています。高度なオプションとしては、スキーマを参照して、そこに定義された型を使用するなどのオプションもありますが、記事で説明するメソッドで使用するのは基本的な型システムのみです。
XSLT 2.0 では新たに、変数およびパラメーターに明示的なデータ型を指定できるようになっています。データ型の指定では、XML Schema
による型システムが基本的に使用され、これを補うために多少の構文とセマンティクスが追加されています。データ型を指定するには、以下のように as 属性を使用します。
<xsl:variable name="TestVariable" as="xs:integer" select="123"/> |
基本的なデータ型には、上記の他に xs:string、xs:boolean、xs:double、xs:date、および xs:dateTime
があります。データ型を指定するには、xs 名前空間接頭辞を何らかの場所 (できればスタイルシートのルート要素) に指定しなければならないことを忘れないでください。
xmlns:xs="http://www.w3.org/2001/XMLSchema" |
XSLT 2.0 では、変数にツリーのフラグメント (文書、ノード、属性など) を格納することも可能です。ほんの少し構文を追加するだけで、フラグメントを格納する変数のデータ型を定義することができます。リスト 3 にいくつかの例を記載します。
リスト 3. ツリーのフラグメントを格納する変数の例
<xsl:variable name="TheCompleteDocument" as="document-node()" select="/"/> <xsl:variable name="AnyElement" as="element()" select="*[1]"/> <xsl:variable name="FirstThingElement" as="element(Thing)" select="/Things/Thing[1]"/> <xsl:variable name="FirstAttribute" as="attribute()" select="@*[1]"/> <xsl:variable name="IdAttribute" as="attribute()" select="@id"/> |
これだけでも素晴らしいことですが、それだけでは終わりません。XSLT 2.0 ではすべての変数がシーケンス (つまり、順序付けられた値のセット)
であり、シーケンスは値を持たないことも 1 つ以上の値を持つこともできます。したがって、「通常の」変数 (上記の例に示されているような変数) は、シーケンスに 1
つの値しか含まれない特殊な例ということになります。シーケンスの威力を活用するには、1 つ以上の値を含める場合は正符号 (+)、値を含めないか 1 つ以上の値を含める場合はアスタリスク (*)、値を含めないか 1 つの値を含める場合は疑問符 (?) を型指定の最後に追加します。これはまさしく、古き良き DTD と同じです。リスト 4 に、これらのシーケンスの例を記載します。
リスト 4. シーケンスの例
<xsl:variable name="MultipleStrings" as="xs:string+" select="('This', 'That')"/>
<xsl:variable name="EmptySetOfIntegers" as="xs:integer?" select="()"/>
<xsl:variable name="SetOfAllThings" as="element(Thing)*" select="//Thing"/>
|
シーケンスとしての変数には、かなりの威力があります。なぜなら、通常のありとあらゆる XPath 構文を変数に指定できるからです。リスト 5 に、一例を記載します。
リスト 5. XSLT 2.0 の変数を使用した XPath 構文
<xsl:for-each select="$SetOfAllThings">
<!-- Do something with the things -->
</xsl:for-each>
<xsl:variable name="LastString" as="xs:string" select="$MultipleStrings[last()]"/>
<xsl:variable name="ThingCount" as="xs:integer" select="count($SetOfAllThings)"/>
<xsl:variable name="AllThingIdAttributes" as="attribute(thingid)*"
select="$SetOfAllThings/@thingid"/>
|
以上で説明したことが、より堅牢なスタイルシートを作成する上でどのように役立つのかわかりますか?型システムの利点は、実行時に XSLT プロセッサーが実際に変数の値をその型に照らし合わせ、変数が型に適合しない場合 (変数の多重度を含む) にはエラーを出すところにあります。したがって、入力文書からの重要な値を、適切な型を指定した変数に取り込めば、何らかの問題がある場合にはエラーを受け取れるというわけです。
XPath の要素名および属性名のタイプ・ミスを (スタイルシートでも、入力文書でも) 防ぐには、どうすればよいのでしょうか?ダブルチェック、読み返す、徹底的なデバッグを行う、幸運を祈る、神に祈る・・・。もちろん、このすべてを行う必要がありますが、本当に必要なことは、バグの存在を非常に目立つエラー・メッセージで示すことです。
リスト 2 のコードをリスト 6 のように作成し直すと、すぐにエラーがポップアップ表示されるようになります。
リスト 6. リスト 2 に変数を追加した XML の例
<xsl:template name="ProcessThing">
<xsl:param name="ThingId"/>
<xsl:variable name="ThingToProcess" as="element(Thing)"
select="/Things/Thing[@id eq $ThingId]"/>
<xsl:for-each select="$ThingToProcess">
<!-- Do something with the thing -->
</xsl:for-each>
</xsl:template>
|
XSLT プロセッサーは実行時に、/Things/Thing[@id eq $ThingId]
によって返されるのが、想定していた Thing 要素ではなく、空のシーケンスであることに気付きます。けれども、ThingToProcess 変数の型定義は element(Thing) となっています。つまり、Thing 要素が 1 つだけ返されなければなりません。したがって、この要素は型定義に適合しないため、エラー・メッセージがポップアップ表示されて変換プロセスが停止することになります。
しかも、このように作成し直したコードにはおまけも付いてきます。それは、入力文書に誤りがある上に 2 つの Thing 要素が同じ ID
を持つ場合にも、エラーが出されることです。$ThingToProcess 変数は 1 つの Thing 要素しか格納することはできません。
このテンプレートの正確性をさらに強化するには、パラメーターのデータ型を以下のように定義します。
<xsl:param name="ThingId" as="xs:string" required="yes"/> |
上記のように required="yes" を追加すると、パラメーターを設定し忘れて空のまま渡した場合、または複数の値を渡した場合にはエラーとしてトラップされます。
この手法は、さまざまな形で利用することができます。例えば、私は大抵、重要なグローバル値はルート要素の属性に取り込むようにしています。これらの値をコード全体でグローバルに使用できるようにするためには、以下のように、あらかじめ最上位レベルの変数に値を格納してからそれらの値を使用するようにします。
<xsl:variable name="GlobalId" as="xs:string" select="/*/@id"/> <xsl:variable name="GlobalName" as="xs:string" select="/*/@name"/> |
xs:string のような汎用的なデータ型であるとしても、変数にデータ型を定義することで、属性の想定外の欠如がトラップされることになります。正直なところ、私はこの方法によって何度も窮地から救われてきました。入力文書からの重要な値は、まず変数に取り込み、これらの変数に対して、適切な型および多重度を持つデータ型を指定してください。
スタイルシートが主に突き合わせを行うためのテンプレートで構成されている場合には、必要なさそうに思えても、catch-all テンプレートの追加を検討してください。
例えば、入力に含まれるすべての要素にマッチするテンプレートを作成し、<xsl:apply-templates>
を使用して入力文書の構造を伝播させるようにしたとします。その場合、入力文書で要素の名前を誤って入力してしまったり、誰かが入力文書を変更したりするとどうなるでしょう?要素のデフォルト・テンプレートが適用されて、暗黙の
<xsl:apply-templates> が実行されます。それが望み通りの場合もあれば、そうではない場合もありますが、何が起こったのかを調べられるように、エラーを受け取るようにしたほうが賢明です。そのためには、リスト 7 のような単純な catch-all テンプレートを使用することができます。
リスト 7. 単純な catch-all テンプレート
<xsl:template match="*">
<xsl:message terminate="yes">
Unexpected element: <xsl:value-of select="name()"/>
</xsl:message>
</xsl:template>
|
名前付きテンプレートとそのパラメーターも問題の原因になります。as 属性を使用してパラメーターにデータ型を指定するだけでも多くのエラーがトラップされますが、それではトラップされないエラーもあります。
XSLT 2.0 では、パラメーター・リストに定義されていないパラメーターを名前付きテンプレートに渡すことはできません。この制約は実のところ、XSLT 1.0 との数少ない非互換性の 1 つですが、パラメーター名のタイプ・ミスを防止するという点で効果的な制約です。
ご存知ないかもしれませんが、名前付きテンプレートをさらに正確にするための秘訣は、XSLT 2.0 に組み込まれている以下の機能を利用することです。必要に応じて、以下のようにパラメーターにフラグを立てることができます。
<xsl:template name="DoSomething">
<xsl:param name="Subject" as="xs:string" required="yes"/>
<!-- ... -->
</xsl:template>
|
上記のコードによって、DoSomething を呼び出すときには Subject パラメーターを指定しなければならなくなります。この手法は、パラメーター・リストが長く、パラメーターを忘れがちな場合にとりわけ役立ちます。
したがって XSLT 2.0 では、DoSomething 名前付きテンプレートが定義されている場合、以下の呼び出しは両方とも不正な呼び出しとしてトラップされます。
<xsl:call-template name="DoSomething">
<xsl:with-param name="subject" select="'Safer stylesheets'"/>
</xsl:call-template>
<xsl:call-template name="DoSomething"/>
|
コードを小さな塊に分割してコードの重複を防ぐ上で、名前付きテンプレートは非常に効果があります。例えば、コード内に Thing 要素を (同じ方法で) 処理する複数の場所があるとしたら、リスト 8 に記載するような名前付きテンプレートを作成することをお勧めします。
リスト 8. コンテキスト要素としての Thing 要素の処理
<xsl:template name="HandleThing">
<!-- Current element must be a <Thing>! -->
<!-- ... -->
</xsl:template>
|
この場合も同じく、現在扱っている要素が Thing でない場合にはエラーが生成されるようにすると賢明かもしれません。それには、リスト 9 のようなコードを使用することができます。
リスト 9. コンテキスト・チェックを伴う Thing 要素の処理
<xsl:template name="HandleThing">
<xsl:param name="ThingToHandle" as="element(Thing)" select="."/>
<xsl:for-each select="$ThingToHandle">
<!-- Now the current element is a <Thing> or we get an error! -->
<!-- ... -->
</xsl:for-each>
</xsl:template>
|
ThingToHandle
は、パラメーターではなく変数として宣言することもできますが、パラメーターを使用することで、思いがけない利点がもたらされます。それは、現在扱っている要素が Thing
でない場合でも、HandleThing を使用できることです。そのために必要なことは、HandleThing
が扱う必要がある要素を ThingToHandle パラメーターに入れて HandleThing に渡すことだけです。
リスト 10. 現行のコンテキストではない Thing 要素の処理
<xsl:template match="/">
<!-- Only handle the first thing: -->
<xsl:call-template name="HandleThing">
<xsl:with-param name="ThingToHandle" select="/*/Thing[1]"/>
</xsl:call-template>
</xsl:template>
|
より正確な XSLT スタイルシートを作成するために最後に紹介する秘訣は、アサートと normalize-space() 関数の 2 つです。
ほとんどのプログラミング言語では、アサートと呼ばれる命令を使用します。アサートとは、特定の条件に合致した場合に
(重要な変数に想定外の値が含まれる場合など)、処理を停止する文のことです。XSLT ではアサートを使いませんが、XSLT
プロセッサーの処理を中断するための手段として、<xsl:message terminate="yes">
と XPath error() 関数を使用することができます。このどちらかを <xsl:if> と組み合わせることで、優れたアサートになります。以下は、その一例です。
<xsl:if test="empty(/*/Thing)">
<xsl:message terminate="yes">No things found in input document</xsl:message>
</xsl:if>
|
error() 関数を使用する場合には、以下のようなコードになります。
<xsl:if test="empty(/*/Thing)">
<xsl:value-of select="error((), 'No things found in input document')"/>
</xsl:if>
|
2 つのうちのどちらを使用するかを決める前に、お使いの IDE とランタイム環境でそれぞれの振る舞いをテストしてください。私のシステムの設定では、error() 関数のほうが有益な結果をもたらします。有益な結果とは、エラー・メッセージの表示です。<xsl:message> は特定の行でエラーが発生したことを通知するものの、エラー・メッセージ自体を表示するこことはしません。
整形出力に対して normalize-space() を使用する
より正確なスタイルシートを作成するための最後の秘訣は、normalize-space() 関数をふんだんに使用することです。その理由は、XML エディターの整形出力機能は、不要な改行や (さらに厄介なことに) ホワイト・スペースを取り込む可能性があるためです。例えば、XML 入力文書のどこかに、以下のような要素が深くネストされているとします。
<DeeplyNestedElement>This is an example of a pretty print error</DeeplyNestedElement> |
無知なコード作成者が整形出力のボタンをクリックすると、上記の要素は途端に以下のようになってしまいます。
<DeeplyNestedElement>This is an example of a pretty
print error</DeeplyNestedElement>
|
このように、コードは改行され、pretty と print の間には多数のスペースが入っています。HTML
を生成するのであれば、これでも問題はありませんが、要素の正確な内容を用いる場合には、この手法では困ったことになります。この問題を修正してくれるのが、normalize-space() です。この関数は要素の前後にあるホワイト・スペースを取り除き、その他のホワイト・スペースのシーケンスをいずれも 1 つのスペースに変換します。
注意する点として、normalize-space() は必要なホワイト・スペースと改行も取り除いてしまう可能性があります。ただし、テキスト内のホワイト・スペースをそのまま利用する入力は、少なくとも私が見てきた世界ではめったにお目にかかりません。
この世界は完璧ではありません。特にプログラミングに関しては、完璧であるとは期待しないでください。この記事では、見過ごされがちなXSLT 処理のエラーをトラップする方法、あるいは防止する方法をいくつか紹介しました。特に型システムに至っては、おそらく意外ながらも極めて有効なエラー・トラップ機能となる可能性があります。
学ぶために
- 『XSLT
2.0 and XPath 2.0, 4th edition』(Michael Kay 著、Wrox、2008年): XSLT 2.0 の型システムについての詳細は、第 5 章を読んでください。
- XSL
Transformations (XSLT) Version 2.0 (W3C Recommendation、2007年1月): XML 文書を別の XML 文書に変換するための言語、XSLT 2.0 の構文およびセマンティクスの詳細を調べてください。
- New to XML で、XML を学ぶために必要なリソースを入手してください。
- developerWorks の XML エリア:
DTD、スキーマ、XSLT を含め、XML 分野でのスキルを磨くための資料を見つけてください。広範な技術に関する記事とヒント、チュートリアル、標準、そして IBM
Redbooks については、XML 技術文書一覧を参照してください。
- IBM XML 認定: XML や関連技術の IBM 認定技術者になる方法について調べてください。
- developerWorks の Technical events and webcasts: これらのセッションで最新情報を入手してください。
- Twitter での developerWorks: 今すぐ登録して developerWorks のツイートをフォローしてください。
- developerWorks
podcasts: ソフトウェア開発者向けの興味深いインタビューとディスカッションを聞いてください。
- developerWorks オンデマンド・デモ: 初心者向けの製品のインストールおよびセットアップから熟練開発者向けの高度な機能に至るまで、さまざまに揃ったデモを見てください。
製品や技術を入手するために
- IBM 製品の評価版:
DB2、Lotus、Rational、Tivoli、および
WebSphere のアプリケーション開発ツールとミドルウェア製品を体験するには、評価版をダウンロードするか、IBM SOA Sandbox のオンライン試用版を試してみてください。
議論するために
- developerWorks
プロフィール: 今すぐ developerWorks で自分のプロフィールを作って、ウォッチ・リストをセットアップしてください。
- XML
ゾーンのディスカッション・フォーラム: XML 関連のフォーラムに参加してください。
- developerWorks コミュニティー: ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者が主導するブログ、フォーラム、グループ、ウィキを調べることができます。

Erik Siegel はオランダでフリーの XML スペシャリストとして活動しています。彼の経歴は研究者、プログラマー、システム・アナリスト、プロジェクト・マネージャー、システム・アーキテクト、コンサルタントなど多岐にわたっています。最近の 5 年間で、XML がそれに加わりました。主な顧客は出版業界で、XML の分野ではコンサルタント、教育、スキーマ開発、および XSLT プログラミングを行っています。Erik と彼の会社についての詳細は www.xatapult.com をご覧ください。