ものがたり(旧)

atsushieno.hatenablog.com に続く

XSD Inference (4)モデルグループとパーティクル

Introduction

第4回は内容モデルのうち、モデルグループとパーティクルの推測に関する話。モデルグループとパーティクルはどちらも内容モデルを表すのに使われるものだけど、complex contentの内容として、complexType, extension, restrictionの各要素の子として出現できるのがモデルグループで、内容グループ自身あるいはその子として出現しうるものがパーティクルで、これらは同じものではない(XML Schema Structuresの3.8と3.9の違い)。*1

サポートされる内容グループ

XSD Inferenceでサポートされる内容グループは、かなり限られている。

まず、出現回数は、0, 1, maxOccursの3種類に限られている(つまりDTDと同じレベル)。これが不便な制約だと感じる人はほとんどいないだろう。逆に、インスタンス上では12回しか出現しなかったからmaxOccurs=12だ、なんて推測をされても困る。

そして内容モデルだが、具体的には以下の2種類しかサポートされていない:

  • xs:elementを含むxs:sequence
  • xs:elementを含む、maxOccurs="unbound"であるxs:choiceを含むxs:sequence

これ以外は全てNGだ。具体的には

  • xs:group ref="..."はサポートされていない
  • xs:allはサポートされていない
  • xs:anyはサポートされていない
  • xs:choice(そのもの)はサポートされていない

そもそもXSD Inferenceでは、最初に挙げた2パターンしか生成されないのだから、それしかサポートされないわけだ。でも、何でこの2つ以外はサポートされないのだろうか?

  • まず、xs:groupの参照を展開するのはかなり面倒な処理になる。また、missing sub componentsを含む妥当なグループ参照が存在しうるので、xs:groupの参照を含むモデルグループは、結果的に正しくないスキーマ コンポーネントになる可能性がある。
  • 次に、xs:allをサポートしない理由は自明ではない(が、あえてサポートするほどの魅力に欠ける内容グループであることは確かだ)。一度出現した要素を追跡しなければならないし、xs:allを抜けた後で出現しなかった要素にminOccurs="0"を追加しなければならなくなる面倒はある。もしxs:allが維持できなくなった場合は、xs:choiceのxs:sequenceに置き換えることは可能だ。
  • xs:anyのサポートは実質的には不可能だ。現在出現すべきパーティクルがxs:anyであるときに、新しい要素が出現したらどうなるか。xs:anyにはwildcardがあって、その要素が常にxs:anyによって受け入れられるわけではない。しかし、親がxs:sequenceであれば、それはxs:anyとして消化するしかなくなる。xs:elementをxs:anyの前に挿入すると、それ以前には存在しなかった要素なのでminOccurs="0"としなければならず、そうするとxs:anyのwildcardを変更して、その要素の名前空間を排除しなければならなくなってしまう(そうしないとunique particle attribution違反となってしまうため)。そうすると、以前にはxs:anyとして許容できていた内容が、許容されなくなってしまう可能性があるのである。そして、逆にxs:anyを拡張すると、今度は以前に推測していた要素との間で、unique particle attribution違反を起こす可能性がある。
  • xs:choiceは、追跡するのが困難になる上(筆者はもう考えるのが面倒になった)、choice内に含まれるパーティクルとの間でunique particle attribution違反を起こす可能性が高くなる。

…というわけで、上記2つのパターンに絞ってサポートする方が現実的だ。(xs:choiceを含むxs:sequenceという書き方は、何となくやぼったく見えるが、なぜこうしたのかはちょっと分かっていない。)

パーティクル推測プロセス

推測の進め方は、ここから上記2パターンに分けられる。その方が簡単だし。基本的には、sequenceとしての推測が不可能になったときに、はじめてchoiceのsequenceに移行するというモデルに基づいていて、choiceのsequenceはいわば最終形態なのである。

maxOccurs="unbounded" であるchoiceのsequenceとなるパターンの場合、まずchoiceに含まれる先頭のelementから、QNameのマッチするものを探す(名前のマッチングについては後の説明も参照)。もし存在すれば、そのelementの定義が現在のXmlReader上のElementを許容するように拡張する(新しいものを定義することはできない; unique particle attribution違反)。もし存在しなければ、新しく追加する。

単純なsequenceの推測はより複雑だ。sequenceの内容は、順序通りに出現しなければならないので、「現在推測中のパーティクルの位置」も重要になる。また、補助的な引数として、「現在推測中のパーティクルは出現したか」を保持しておくことも必要になる(これを仮に「出現フラグ」と呼ぼう)。

まず、sequenceに含まれるelement(これ以外は許容されていないことに注意)の名前にマッチするものを、「先頭から現在推測中のパーティクルの位置マイナス1まで」探す(最初はこの探索が行われない)。もし同じQNameをもつ要素が出現してしまったら、この時点で、このsequenceは維持することができない*2。なぜなら、XML Schema Structures 3.8.6の制限により、同じQNameをもつelementがあるモデルグループの子孫として含まれている場合、その型は同一でなければならない、という制約があり、同じQNameをもつelementについて、もし結果的に違うelementが推測されてしまったら、このモデルグループは不正なスキーマ コンポーネントになってしまうからだ(InferenceはXmlReaderベースで行われるので、バックトラッキングができないことも問題になる*3)。

もしここまでで名前の一致するelementパーティクルが出現しなければ、「現在推測中のパーティクル」のQNameとelementを比較する。もし一致した場合、ここで「出現フラグ」がonになっていなかったら、「出現フラグ」をonにし、もしonになっていたら、maxOccursをunboundedにする。そして、その要素型が現在のXmlReader上のElementを許容するように拡張する。もしQNameが一致しなければ、ここで「出現フラグ」がonであれば、そのパーティクルは正常に推測されていたということなので、「出現フラグ」をoffにして、出現フラグがoffであれば、この最後のパーティクルは出現しなかったということになる。ここでの処理には2パターン考えられるが、おそらくMS.NETではminOccursを0にし、monoでは単純にsequenceとしてはもはや失敗したとみなしてchoiceに移行する。

elementパーティクルのマッチング

elementパーティクルを探索するとき、外部名前空間に属する要素はNameではなくRefNameを調べなければならないことに注意しなければならない。このことは、現在のモデルグループを含むcomplexTypeを含むXmlSchemaのTargetNamespaceが参照される、ということを意味している。

これがコンパイルされたXmlSchemaElementであれば、QualifiedNameプロパティを参照することができるが、推測中のパーティクルはコンパイルされていないので、QualifiedNameを使うことはできない。

*1:たとえばxs:elementやxs:anyは内容グループではないので、xs:complexType/xs:element というパスはあり得ない。

*2:本当は、続くパーティクルの出現を調べて、もし出現しなければ、以降のパーティクルのminOccursを0にすることで、sequenceを維持することはできる。MS.NETとmonoの実装の間で、ここに微妙な違いがあるように思えるが、具体的な境界条件は未調査だ。

*3:まあこれは実装の都合の問題ではあるけれど。