scalaxb 0.5.2
<xs:sequence>の取り扱いを修正しました。(GH-19)<xs:attributeGroup>の取り扱いを修正しました。 (GH-21)- 参照されたスキーマやコンポーネントが見つからない場合のエラーメッセージを改善しました。
- 見つからないスキーマを現ディレクトリ内で検索するようにしました。(GH-23)
<xs:sequence> の取り扱いを修正しました。(GH-19)<xs:attributeGroup> の取り扱いを修正しました。 (GH-21)This is a memo to navigate around the case classes for XML Schema. Besides the derivation by extension that programming languages have as inheritance, XML Schema also allows derivation by restriction. Since Scala I don’t think expresses this easily, some types are wider than how they are specified in the schema.
xelementoption : Option[DataRecord[XElementOption]]name: Option[String]ref : Option[QName]typeValue : Option[QName]known subclasses: XTopLevelSimpleType, XLocalSimpleType
value member: arg1 : DataRecord[XSimpleDerivationOption]
simpleType : Seq[XLocalSimpleType]memberTypes : Option[Seq[QName]]arg1 : XSimpleRestrictionModelSequencesimpleType : Option[XLocalSimpleType]arg2 : Seq[DataRecord[XFacetsOption]]結局の所,scalaxb のユーザはエンティティ・オブジェクトが表現する現実の問題に興味があるのであって,それがどう XML に永続化されるかといったことではない.だから,いつかデータバインディングの実装をシングルトン/コンパニオン・オブジェクトから追い出さなければいけないことは分かっていた.つい最近までデータバインディングの実装は以下のように生成されていた:
object Address extends rt.ElemNameParser[Address] {
val targetNamespace = "http://www.example.com/IPO"
def parser(node: scala.xml.Node): Parser[Address] =
...
def toXML(__obj: Address, __namespace: String, __elementLabel: String, __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq =
...
}
つまり,scalaxb は Address そのものとは関係の無い XML データバインディングのために一等地をハイジャックしてしまったのだ.
まず最初に以下のような adapter オブジェクトに追い出すことを考えた:
object DefaultXMLAdapter {
object AddressAdapter extends rt.ElemNameParser[Address] {
val targetNamespace = "http://www.example.com/IPO"
def parser(node: scala.xml.Node): Parser[Address] =
...
def toXML(__obj: Address, __namespace: String, __elementLabel: String, __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq =
...
}
}
この方法にはいくつかの問題がある.まず,scalaxb のランタイムである DataRecord が,今まではコンパニオン・オブジェクトの暗黙性を使ってたどっていた toXML にたどり着けないということだ.コンパニオン・オブジェクトの興味深い一面として「コンパイラは,暗黙 (implicit) の定義を変換元の型と変換先の型のコンパニオン・オブジェクトにも探しにいく」(Programming in Scala, p. 441) というものがある.
第二の問題は,アイデンティティー問題だ.ユーザのコードの邪魔にならないようにしようとしているのに,Address オブジェクトのためには DefeaultXMLAdapter.AddressAdapter,Item のためには DefeaultXMLAdapter.ItemAdapterなどと, かえって目に障るものを導入してしまった.ユーザが知っている必要があるのは Address が XML に変換できるという事実だけあって,それがどう行われているかというのは余計な詳細でしかない.
第三の問題として,拡張性の問題がある.例えば,Address を定義する ipo.xsd と,Address を使う purchaseReport要素を定義する report.xsd の二つのスキーマがあるとする.問題は,report.DefaultXMLAdapter.PurchaseReportAdapter は ipo.DefaultXMLAdapter.AddressAdapter を参照するため,ipo.DefaultXMLAdapter を拡張してカスタムのデータバインディングをすることができないということだ.
I presented a 5-min lightening talk at 2010-11-29 New York Scala Enthusiasts meetup with 8 others. Here’s the pdf of the slides. The presentation fell apart in the middle because the sample code text was too dark over the black background (corrected in pdf). Nonetheless it was inspiring to get live feedback and see what other hackers are up to.
xmlprotocol.scala という別のファイルに生成します.org.scalaxb.rt から scalaxb という名前に変更して,また補助ファイルを scalaxb.scala という名前に変えました.22 のアイテムを含む
<xs:all>の取り扱いを修正しました (GH-10).
22 の属性を含む
<xs:complexType>の取り扱いを修正しました (GH-11).
<xs:complexContent> を制限する <xs:simpleContent> を含んだ <xs:complexType> の取り扱いを修正しました (GH-12).<xs:import> の取り扱いを修正しました (GH-14).<xs:include> に指定されたスキーマがコンパイルに含まれていない場合に警告を出すようにしました (GH-15).ご意見,バグレポートなどをくださった方に感謝の意を表したいと思います.特に Debasish Ghoshさんの記事からはプログラミングに関する洞察をいただき,Andreas Sewe さんからは上記の全てのバグ報告をいただき,ありがとうございました.
今回特に重要な変更点は XML データバインディングのコードが case class のコンパニオンオブジェクトから別の XMLProcotol という trait に抽出されたことです.これにより,使用コードは以下のようになります:
import scalaxb._
import Scalaxb._
import ipo._
import DefaultXMLProtocol._
val shipTo = fromXML[Address](<shipTo>
<name>foo</name>
<street>1537 Paper Street</street>
<city>Wilmington</city>
</shipTo>)
shipTo は以下の値なります:
パーサ・コンビネータを使って混在内容のコード生成を改良しました.
XML Schema 組み込み型を値の場合の <any> のデータ・バインディングを改良しました.
選択肢が単一のクラスか trait に限定される場合の <choice> のコード生成を改良しました.
コード生成をカスタマイズする –class-prefix と –param-prefix のオプションを加えました.
デプロイ時の問題を回避するため scala-library を jar に含めました.
恐らく Scala バージョンが合わなかったことに起因する sbaz を使った scalaxb インストールの問題が前に何例か報告されたので scala-library を sbt-proguard-plugin を使って scalaxb.jar の中に入れてしまうことにしました.
生成されるコードにプリフィックスを加えるため,–class-prefix と –param-prefix の二つのオプションを追加しました.主要なスキーマのように要素名に小文字を使っていればこのオプションは必要になることはないと思います.scalaxb はクラス名の語頭を大文字にしますが,パラメータ名はいじらないため,以下のようなコードになるためです:
case class USAddress(name: String,
street: String,
city: String,
usstate: USState,
zip: Int) extends Addressable
要素名の語頭がスキーマの中でも大文字化されているとすると,以下のようなコードになってしまいます:
case class USAddress(Name: String,
Street: String,
City: String,
USState: USState,
Zip: Int) extends Addressable
これは合法なコードですが USState が二重の意味を持つことは混乱の原因となります.ここで --param-prefix を使ってパラメータにプリフィックスを付けることができます.例えば,m を付けるとすると,以下のコードを生成します:
case class USAddress(mName: String,
mStreet: String,
mCity: String,
mUSState: USState,
mZip: Int) extends Addressable
また,何らかの理由でクラス名にもプリフィックスを付けたい場合は --class-prefix を使います.X を選ぶとすると,以下のコードを生成します:
case class XUSAddress(mName: String,
mStreet: String,
mCity: String,
mUSState: XUSState,
mZip: Int) extends XAddressable
小文字を名前にプリフィックスを付けるのにちょっとした細工を実装してみました.scalaxb はプリフィックスを付ける前に元の名前の語頭を大文字化しますが,プリフィックスが _ で終わる場合は大文字化しないことにしました.最初の例に m をプリフィックスする場合は,以下のコードを生成します:
case class USAddress(mName: String,
mStreet: String,
mCity: String,
mUsstate: USState,
mZip: Int) extends Addressable
しかし,m_ を選んだ場合は,以下のようになります:
case class USAddress(m_name: String,
m_street: String,
m_city: String,
m_usstate: USState,
m_zip: Int) extends Addressable
XML ドキュメントを利用しやすいネイティブなオブジェクトに変換(データ・バインディング)する,そしてそのオブジェクトを XML ドキュメントに逆変換(ラウンド・トリップ)できる能力を保持するという二つの要請から rt.DataRecord は 生まれました.しかし,この二つの要件は時として統一的な行動を取りません.例えば,逆変換のことだけを考えてしまうと,XML を書き出すために scala.xml.Node を保持すれば解決してしまいますが,データ・バインディングという立場から見ると便利なものではありません.
scalaxb の当初の目標は XML Schema の表現空間をカバーして,かつラウンド・トリップを実装することでした.この目標はほぼ達成されたので,最近の更新では逆変換性は保持しつつ,生成されたコードの利便性を高めることを目指してきました.ラウンド・トリップ側に傾き過ぎたものの一つに <choice> のコード生成があります.まずはこれまでの <choice> のコード生成の歴史を振り返り,最後に新しい案を提起したいと思います.
<any> のラウンド・トリップにおいて貨物を無くさないために,DataRecord中に scala.xml.Elem を保存してきましたが,DataRecord を利用する側に立つと,これは不便でもあります.問題は <any> はどの要素が来てもいいためパース不可能なことです.ただし,組み込み型はパース可能です.
混在内容のカタがある程度ついたので,XSD 組み込み型のパースに手をつけるいい機会だと思いました.使用例は以下のようになります:
def testAny {
val subject = <foo xmlns="http://www.example.com/any"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<int xsi:type="xs:int">1</int>
<byte xsi:type="xs:byte">1</byte>
<dateTime xsi:type="xs:dateTime">2002-10-10T12:00:00Z</dateTime>
</foo>
val obj = Element1.fromXML(subject)
obj match {
case Element1(
DataRecord(Some("http://www.example.com/any"), Some("int"), 1),
DataRecord(Some("http://www.example.com/any"), Some("byte"), 1),
DataRecord(Some("http://www.example.com/any"), Some("dateTime"), XMLCalendar("2002-10-10T12:00:00Z"))
) =>
case _ => error("match failed: " + obj.toString)
}
val document = Element1.toXML(obj, None, Some("foo"), subject.scope)
println(document)
}
DataRecord は Int,Byte,javax.xml.datatype.XMLGregorianCalendar,など組み込み型に対応した適当な型を格納します.DataRecord はまた内部的に {http://www.w3.org/2001/XMLSchema-instance}type 属性付きでラウンド・トリップする方法を記憶しています.
混在内容のサポートを前に始めましたが,おさらいをしておくと,<xs:complexType mixed="true"> の時,XHTML のように一つの要素内にテキストノードと子要素が並列して混在することができようになります.実装した後で,生成される case class が DRY じゃないのが結構気になっていました.
例えば,
<xs:element name="mixedTest">
<xs:complexType mixed="true">
<xs:choice maxOccurs="unbounded">
<xs:element name="billTo" type="Address"/>
<xs:any namespace="##other" processContents="lax" />
</xs:choice>
</xs:complexType>
</xs:element>
は case class Element3(arg1: Seq[rt.DataRecord[Any]], mixed: Seq[rt.DataRecord[Any]]) 生成していましたが,最初のパラメータの arg1 は子要素を収容し,次の mixed は子要素とテキスト・ノードの両方を収容します.case class が XML に往復して帰るためにはテキスト・ノードと子要素の両方を順番通りに mixed に保存する必要があります.しかし,パーシングは子要素のみに行われていたため,arg1のみに Address オブジェクトは格納され,mixed にはパース前の scala.xml.Elem オブジェクトが入っています.お行儀が悪いですね.
以前のパーシングがあまり洗練されたものではなかったのが,一つの理由になっています.
いよいよマトモなパーサを使ったパーシングを始めたので,この問題を再検討することにしました.順番を保存する必要性は変わらないので mixed は今後も必要になります.しかし,もしパーシングがしっかりすれば arg1 は無くすことができるのではないでしょうか.そのために複合型が混在であるときはテキスト・ノードも文法の一部とみなすようにパーシングを変更しました.上記の複合型より生成されたコードです:
case class MixedTest(mixed: Seq[rt.DataRecord[Any]])
object MixedTest extends rt.ElemNameParser[MixedTest] {
val targetNamespace: Option[String] = Some("http://www.example.com/mixed")
def isMixed: Boolean = true
def parser(node: scala.xml.Node): Parser[MixedTest] =
optTextRecord ~ rep((((((rt.ElemName(targetNamespace, "billTo")) ^^
(x => rt.DataRecord(x.namespace, Some(x.name), Address.fromXML(x.node)))) ~
optTextRecord) ^^
{ case p1 ~ p2 => Seq.concat(Seq(p1),
p2.toList) })) |
(((any ^^ (x => rt.DataRecord(x.namespace, Some(x.name), x.node))) ~
optTextRecord) ^^
{ case p1 ~ p2 => Seq.concat(Seq(p1), p2.toList) })) ~ optTextRecord ^^
{ case p1 ~
p2 ~
p3 => MixedTest(Seq.concat(p1.toList,
p2.flatten,
p3.toList)) }
def toXML(__obj: MixedTest, __namespace: Option[String], __elementLabel: Option[String], __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq = {
var attribute: scala.xml.MetaData = scala.xml.Null
scala.xml.Elem(rt.Helper.getPrefix(__namespace, __scope).orNull,
__elementLabel getOrElse { error("missing element label.") },
attribute, __scope,
__obj.mixed.flatMap(x => rt.DataRecord.toXML(x, x.namespace, x.key, __scope).toSeq): _*)
}
}
住所オブジェクトもちゃんとパースされて,しかも重複せずに保存されています.一見問題が解決したかに思われましたが,ラウンド・トリップにおいて新たな問題が発生しました.scala.xml.Elem を保持しないために,DataRecord.toXML が XML を生成できなくなってしまったのです.mixed は Int や String のような組み込み型 ,scala.xml.Elem のような XMLノード,そして Address のようなユーザ定義型を格納できるように rt.DataRecord[Any] と宣言されています.組み込み型や XMLノードのための XML生成の方法は事前に出荷できますが,ユーザ定義型もサポートする必要があります.型クラスで実装してみることにしました:
<enumeration> のサポートを実装しました.今年の初めに Scala の限界に関して書きました:
Scala は
Tuple22までしかサポートしません.
これは Scala が 22個のパラメータを超えた case class を定義できないことを意味します.22個を超えたパーティクルを持つシーケンスを含む複合型を定義するスキーマを扱えるよう回避策をいくつか用意しました.
まず,シーケンスが MaxParticleSize (20)個以上のパーティクルを持つとき,シーケンスを ChunkParticleSize (10)個ごとのチャンク(塊)に分けることにしました.例えば,30個の要素を持つシーケンスは 10個づつの要素を持った三つのチャンクに分かれます:
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="string1" type="xs:string"/>
<xs:element name="string2" type="xs:string"/>
<xs:element name="string3" type="xs:string"/>
...
<xs:element name="string30" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
scalaxb は以上の要素から以下のコードを生成します:
case class Foo(arg1: FooSequence2,
arg2: FooSequence3,
arg3: FooSequence4) {
def string1 = arg1.string1
def string2 = arg1.string2
def string3 = arg1.string3
def string4 = arg1.string4
def string5 = arg1.string5
def string6 = arg1.string6
def string7 = arg1.string7
def string8 = arg1.string8
def string9 = arg1.string9
def string10 = arg1.string10
def string11 = arg2.string11
def string12 = arg2.string12
def string13 = arg2.string13
def string14 = arg2.string14
def string15 = arg2.string15
def string16 = arg2.string16
def string17 = arg2.string17
def string18 = arg2.string18
def string19 = arg2.string19
def string20 = arg2.string20
def string21 = arg3.string21
def string22 = arg3.string22
def string23 = arg3.string23
def string24 = arg3.string24
def string25 = arg3.string25
def string26 = arg3.string26
def string27 = arg3.string27
def string28 = arg3.string28
def string29 = arg3.string29
def string30 = arg3.string30
}
case class FooSequence2(string1: String,
string2: String,
string3: String,
string4: String,
string5: String,
string6: String,
string7: String,
string8: String,
string9: String,
string10: String)
case class FooSequence3(string11: String,
string12: String,
string13: String,
string14: String,
string15: String,
string16: String,
string17: String,
string18: String,
string19: String,
string20: String)
case class FooSequence4(string21: String,
string22: String,
string23: String,
string24: String,
string25: String,
string26: String,
string27: String,
string28: String,
string29: String,
string30: String)
拡張によって複合型が 22個のパーティクルを超えることがあります.XML Schema は型システムの継承に似て,複合型を他の型から拡張することができます.一方,case class を継承することはできません.
XML Schema には特定の要素を他の要素に差し替える代替グループという機構を持つ.具体的には,ヘッド要素と呼ばれる特定の要素に代替可能な特別な要素グループに他の要素を割り当てることができる.(ただし,ヘッド要素と代替可能な要素はグローバル要素に限る)
以下に代替グループの例を示します:
<complexType name="GH6Usage">
<sequence>
<element ref="ipo:gh6head"/>
<element name="gh6head2" type="string" />
<element name="city" type="string"/>
</sequence>
</complexType>
<element name="gh6head" type="string" abstract="true"/>
<element name="gh6sub1" type="string" substitutionGroup="ipo:gh6head" />
<element name="gh6sub2" type="string" substitutionGroup="ipo:gh6head" />
<element name="gh6head2" type="string" abstract="true"/>
<element name="gh6sub3" type="string" substitutionGroup="ipo:gh6head2" />
<element name="gh6sub4" type="string" substitutionGroup="ipo:gh6head2" />
ここには ipo:gh6head と ipo:gh6head2 という二つの代替グループがありますが,GH6Usage で使われているのは ipo:gh6head のみです.二つ目の要素 gh6head2 は ipo:gh6head2 と同じ名前が付けられていますが,GH6Usage のローカル要素です.以下が scalaxb が生成するコードです:
case class GH6Usage(gh6head: rt.DataRecord[Any],
gh6head2: String,
city: String)
object GH6Usage extends rt.ElemNameParser[GH6Usage] {
val targetNamespace: Option[String] = Some("http://www.example.com/IPO")
def parser(node: scala.xml.Node): Parser[GH6Usage] =
(((rt.ElemName(targetNamespace, "gh6sub2")) ^^
(x => rt.DataRecord(x.namespace, Some(x.name), x.node.text))) |
((rt.ElemName(targetNamespace, "gh6sub1")) ^^
(x => rt.DataRecord(x.namespace, Some(x.name), x.node.text)))) ~
(rt.ElemName(targetNamespace, "gh6head2")) ~
(rt.ElemName(targetNamespace, "city")) ^^
{ case p1 ~
p2 ~
p3 => GH6Usage(p1,
p2.text,
p3.text) }
def toXML(__obj: GH6Usage, __namespace: Option[String], __elementLabel: Option[String]): scala.xml.NodeSeq = {
var __scope: scala.xml.NamespaceBinding = scala.xml.TopScope
__scope = scala.xml.NamespaceBinding("xsi", "http://www.w3.org/2001/XMLSchema-instance", __scope)
__scope = scala.xml.NamespaceBinding("ipo", "http://www.example.com/IPO", __scope)
__scope = scala.xml.NamespaceBinding(null, "http://www.example.com/IPO", __scope)
val node = toXML(__obj, __namespace, __elementLabel, __scope)
node match {
case elem: scala.xml.Elem =>
elem % new scala.xml.PrefixedAttribute(__scope.getPrefix(rt.Helper.XSI_URL),
"type",
"ipo:GH6Usage", elem.attributes)
case _ => node
}
}
def toXML(__obj: GH6Usage, __namespace: Option[String], __elementLabel: Option[String], __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq = {
var attribute: scala.xml.MetaData = scala.xml.Null
scala.xml.Elem(rt.Helper.getPrefix(__namespace, __scope).orNull,
__elementLabel getOrElse { error("missing element label.") },
attribute, __scope,
Seq.concat(rt.DataRecord.toXML(__obj.gh6head, targetNamespace, __obj.gh6head.key, __scope),
scala.xml.Elem(rt.Helper.getPrefix(None, __scope).orNull, "gh6head2", scala.xml.Null, __scope, scala.xml.Text(__obj.gh6head2.toString)),
scala.xml.Elem(rt.Helper.getPrefix(None, __scope).orNull, "city", scala.xml.Null, __scope, scala.xml.Text(__obj.city.toString))): _*)
}
}
enumeration(列挙)ファセットは単純型を具体的な値の集合へと限定します.例えば,アメリカの州の略称を値とするUSStateという新しい単純型をstringから派生して定義するのにenumerationファセットを利用することができます:
<xsd:simpleType name="USState">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="AK"/>
<xsd:enumeration value="AL"/>
<xsd:enumeration value="AR"/>
<!-- and so on ... -->
</xsd:restriction>
</xsd:simpleType>
このような制限は case object を使って表現することができます.以下は scalaxb が生成するコードです:
trait USState
object USState {
def fromXML(seq: scala.xml.NodeSeq): USState = fromString(seq.text)
def fromString(value: String): USState = value match {
case "AK" => AK
case "AL" => AL
case "AR" => AR
case "DE" => DE
}
}
case object AK extends USState { override def toString = "AK" }
case object AL extends USState { override def toString = "AL" }
case object AR extends USState { override def toString = "AR" }
scalaxb-appengine は scalaxb を web上で使うための RESTful API です.n8han/Unfiltered を使ってて,ソースは eed3si9n/scalaxb-appengine にあります.
arg: 省略可,複数化.スキーマファイルの URL.argf: 省略可,複数化.アップロードするスキーマファイル.defaultPackageName: 省略可.対象パッケージ.namespaceURI: 省略可,複数化.パッケージする名前空間 URI.packageName: 省略可,複数化.namespaceURI によって指定された名前空間の対象パッケージ.classPrefix: 省略可,生成されるクラス名のプレフィックスを指定します.paramPrefix: 省略可,生成される仮引数名のプレフィックスを指定します.wrapContents: 省略可,複数化.別の case class に追い出す複合型を指定します.arg か argf かのどちらか一つは指定して下さい.packageName が n番目の namespaceURI の対象パッケージを指定します.<group>, <attributeGroup>, <all> のサポートを実装しました.toXMLがコンパニオンオブジェクトに生成されるようになりました.例えば,
<xs:complexType name="Address">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="street" type="xs:string"/>
<xs:element name="city" type="xs:string"/>
</xs:sequence>
</xs:complexType>
から以下のようなコードが生成されます:
case class Address(name: String,
street: String,
city: String)
モデルグループ定義を行うスキーマコンポーネントの XML 表現は
<group>要素である.これはXMLで表現された複合型定義内においてモデルグループの参照するための名前付けを行うためのものである.
<group> という機構を使うことによってスキーマ設計者は内容モデルの一部を型派生に頼ることなく再利用することができます.これは込み入ったスキーマを扱うときに便利なものです.例えば,以下のような head.misc という名前のグループと head という名前の要素を考えます.
<group name="head.misc">
<sequence>
<choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="ipo:script"/>
<xs:element ref="ipo:style"/>
</choice>
</sequence>
</group>
<element name="head">
<annotation>
<documentation>
content model is "head.misc" combined with a single
title and an optional base element in any order
</documentation>
</annotation>
<complexType>
<sequence>
<group ref="ipo:head.misc"/>
<choice>
<sequence>
<xs:element ref="ipo:title"/>
<group ref="ipo:head.misc"/>
</sequence>
<sequence>
<xs:element ref="ipo:base"/>
<group ref="ipo:head.misc"/>
<xs:element ref="ipo:title"/>
<group ref="ipo:head.misc"/>
</sequence>
</choice>
</sequence>
<attributeGroup ref="ipo:i18n"/>
<attribute name="id" type="ID"/>
<attribute name="profile" type="anyURI"/>
</complexType>
</element>
ちょっとバカバカしいぐらいですが,実際に現場で使われるスキーマはこのぐらい複雑なものもあります.例えば,上の例は XHTML 1.0 Strict 規格から持ってきたものです.以下が生成されたコードの対応部分です:
xsi:nillableのサポートを実装しました (GH-3).生成されるパーサの一例です:
object Address extends rt.ElemNameParser[Address] {
val targetNamespace = "http://www.example.com/IPO"
def parser(node: scala.xml.Node): Parser[Address] =
(rt.ElemName(targetNamespace, "name")) ~
(rt.ElemName(targetNamespace, "street")) ~
(rt.ElemName(targetNamespace, "city")) ^^
{ case p1 ~
p2 ~
p3 => Address(p1.text,
p2.text,
p3.text) }
}
XML において値の欠如を表現するのに要素を省くか空要素を使うかの方法があります.
未配送の物品,不明な情報,不適切な情報などを要素の欠落ではなく明示的に表現することが望ましい場合があります.
空要素 <foo></foo> を使うことの問題は,空の値が指定されている xs:positiveInteger にマッチしなくなってしまうことです.確かに,正の整数と空文字の両方を許容するために,xs:positiveInteger と空文字のみを含んだ xs:enumeration(列挙型)の xs:union(ユニオン型) を形成することも可能ですが,厳密には空文字と純粋な無の値は別物です.コードでいうと,null と "" の違い,特に Scala での None と Some("") の違いがあります.
XML Schema ではこの問題を解決するために xsi:nil という特別な属性を使います.
<price xsi:nil="true" />
と書くことで,XML Schema対応のパーサはこれが特別な空要素でその値が空文字ではなくnil値であると解釈します.ある要素において,可空性(nillability, 要素が空の値を取りうるか)と数性(cardinality, 要素が何個現れるか)は別問題であることに注意してください.つまり,minOccurs=0 で maxOccurs=1 のオプショナルでかつ nillable な要素を定義することもできるし,また,maxOccurs="unbounded" で nillable な要素を定義することもできます.そのような要素を使ってドキュメントの一例を示します:
<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<price xsi:nil="true" />
<price>100</price>
<price xsi:nil="true" />
</foo>
生成されたScalaコードにおいてnullと非nullのチェックを必要以上に導入したくなかったため,<price xsi:nil="true" /> は None 扱いされ,これは minOccurs=0 要素において要素が欠落している場合と同じ値になります.
<xs:element name="NillableTest">
<xs:complexType>
<xs:sequence>
<xs:element name="price" type="xs:positiveInteger" nillable="true"/>
<xs:element name="tax" type="xs:positiveInteger" minOccurs="0" nillable="true"/>
<xs:element name="tag" type="xs:positiveInteger" minOccurs="0" maxOccurs="unbounded" nillable="true"/>
<xs:element name="shipTo" type="ipo:Address" nillable="true"/>
<xs:element name="billTo" type="ipo:Address" minOccurs="0" nillable="true" />
<xs:element name="via" type="ipo:Address" minOccurs="0" maxOccurs="unbounded" nillable="true" />
</xs:sequence>
</xs:complexType>
</xs:element>
以上の定義の要素は以下のコードを生成します:
ハンド・パーシング(手書き構文解析)の限界は前から分かっていました.トークンの位置に頼ったパーシングは,文法に繰り返し,オプション,シーケンスの選択などの複雑なものが出てきたとたんに手に負えなくなります.ある時点で scala のパーサ・コンビネータを使って内容モデルをパーシングをしようと決めましたが,結構長く時間がかかってしまいました.
まずは,実際に使われているスキーマから複雑な構造を紹介しましょう:
<complexType name="SubjectType">
<choice>
<sequence>
<choice>
<element ref="saml:BaseID"/>
<element ref="saml:NameID"/>
<element ref="saml:EncryptedID"/>
</choice>
<element ref="saml:SubjectConfirmation" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
<element ref="saml:SubjectConfirmation" maxOccurs="unbounded"/>
</choice>
</complexType>
選択肢の中にシーケンスがあり,さらにその中に選択肢があります.さて,0.0.3 だとこれをどのように扱うでしょう.
case class SubjectType(arg1: org.scalaxb.rt.DataRecord[Any]) extends org.scalaxb.rt.DataModel {
....
}
生成された case class には rt.DataRecord[Any] しかありません.二番目の選択肢が saml:SubjectConfirmation の繰り返しである可能性を考えると,これは間違いでしょう.
object SubjectType {
def fromXML(node: scala.xml.Node): SubjectType =
SubjectType(SubjectTypeOption.fromXML(node.child.filter(_.isInstanceOf[scala.xml.Elem])(0)))
}
パーシングにも問題があります.まず,上の方法では最初の子要素しか読み込んでいませんが,選択肢の両者とも複数の要素を扱う必要があります.
object SubjectTypeOption {
def fromXML: PartialFunction[scala.xml.NodeSeq, org.scalaxb.rt.DataRecord[Any]] = {
case x: scala.xml.Elem if (x.label == "SubjectConfirmation" &&
x.scope.getURI(x.prefix) == "urn:oasis:names:tc:SAML:2.0:assertion") =>
org.scalaxb.rt.DataRecord(x.scope.getURI(x.prefix), x.label, SubjectConfirmationType.fromXML(x))
}
}
パーシングの限界は SubjectTypeOption.fromXML の実装が一つの要素しか扱っていないことのにも起因しています.つまり,複数の要素を含んだ選択肢は完全に無視されているのです.
<any>,<anyAttribute>,および混在内容のサポートを実装しました.以下ラウンド・トリップの例です:
val subject = <foo xmlns="urn:example:ipo"
xmlns:ipo="urn:example:ipo">
foo<Choice2>2</Choice2>bar</foo>
val obj = Element3.fromXML(subject)
val document = obj.toXML(null, "foo", subject.scope)
一つの要素内にテキストノードと子要素が並列して混在している混在内容のサポートを実装しました.<any>と同様にテキストは他のDataRecordオブジェクトと一緒にDataRecord[String]オブジェクトの中に保存され,mixedという値で呼ばれます.
次のようなスキーマがあるとすると,,
<element name="Element3">
<complexType mixed="true">
<choice maxOccurs="unbounded">
<element ref="ipo:Choice1"/>
<element ref="ipo:Choice2"/>
<any namespace="##other" processContents="lax" />
</choice>
</complexType>
</element>
使用例のコードは以下のようになります.
val subject = <foo xmlns="http://www.example.com/IPO"
xmlns:ipo="http://www.example.com/IPO">foo<Choice2>2</Choice2>bar</foo>
val obj = Element3.fromXML(subject)
obj match {
case Element3(Seq(DataRecord("http://www.example.com/IPO", "Choice2", 2)),
Seq(DataRecord(null, null, "foo"),
DataRecord("http://www.example.com/IPO", "Choice2", 2),
DataRecord(null, null, "bar"))) =>
case _ => error("match failed: " + obj.toString)
}
val document = obj.toXML(null, "foo", subject.scope)
今後の予定としては,このジェネリックなコンテナを使って現在切り捨てている
<any>をちゃんと変換することです.おそらくscala.xml.Elemをそのままの形でコレクションの中に残しておくような形になると思います.一旦ラウンド・トリップ機能を提供すると,ユーザは途中で荷物が無くならないことを期待すると思うので.
と書いたとおり, <any>の処理を変更しました.<any>の処理には2パターンあって,まずsequence の一部として使われる場合.
<element name="Choice1">
<complexType>
<sequence>
<any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
</complexType>
</element>
scalaxb は以下のコードを生成します:
case class Choice1(any: org.scalaxb.rt.DataRecord[scala.xml.Node]*) extends org.scalaxb.rt.DataModel {
def toXML(namespace: String, elementLabel: String, scope: scala.xml.NamespaceBinding): scala.xml.Node = {
val prefix = scope.getPrefix(namespace)
var attribute: scala.xml.MetaData = scala.xml.Null
scala.xml.Elem(prefix, elementLabel,
attribute, scope,
Seq(any.map(x => x.toXML(x.namespace, x.key, scope))).flatten: _*)
}
}
object Choice1 {
def fromXML(node: scala.xml.Node): Choice1 =
Choice1((node.child.partialMap {
case x: scala.xml.Elem =>
org.scalaxb.rt.DataRecord(x.scope.getURI(x.prefix), x.label, x) }).toList: _*)
}
次にchoice の選択肢として使われる場合.
<element name="Element1">
<complexType>
<sequence>
<choice maxOccurs="unbounded">
<element ref="ipo:Choice1"/>
<any namespace="##other" processContents="lax" />
</choice>
</sequence>
</complexType>
</element>
scalaxb は以下のコードを生成します:
ラウンド・トリップ(往復券)の実装を始めています.つまり,xmlドキュメントからscalaオブジェクトに変換して,そこからまたxmlドキュメントに変換できるようにする機能です.以下のようなドキュメントがあるとして,
val subject = <shipTo xmlns="http://www.example.com/IPO"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ipo="http://www.example.com/IPO"
xsi:type="ipo:USAddress">
<name>Foo</name>
<street>1537 Paper Street</street>
<city>Wilmington</city>
<state>DE</state>
<zip>19808</zip>
</shipTo>
fromXML()を呼び出すことでオブジェクトに変換できます
val obj = Addressable.fromXML(subject)
でさらにtoXML()を呼び出すことでxmlドキュメントに再変換できます
obj match {
case usaddress: USAddress =>
val document = usaddress.toXML("shipTo")
println(document)
case _ => error("parsed object is not USAddress")
}
これは以下のxmlドキュメントを生成します:
<shipTo xsi:type="ipo:USAddress" xmlns="http://www.example.com/IPO" xmlns:ipo="http://www.example.com/IPO" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><name>Foo</name><street>1537 Paper Street</street><city>Wilmington</city><state>DE</state><zip>19808</zip></shipTo>
意味論的には元のドキュメントと同一のものですね.ラウンド・トリップを実装してて再度気付かされたのが,xmlでは型の他に要素名も情報を持つということです.以下XML暗号より抜粋:
<element name='ReferenceList'>
<complexType>
<choice minOccurs='1' maxOccurs='unbounded'>
<element name='DataReference' type='xenc:ReferenceType'/>
<element name='KeyReference' type='xenc:ReferenceType'/>
</choice>
</complexType>
</element>
DataReferenceとKeyReferenceの両要素ともxenc:ReferenceTypeの型ですが,オブジェクトが型情報しか保存しない場合,xmlドキュメントに変換する時にどっちの要素に戻っていいのか分からなくなってしまいます.この曖昧さはオブジェクトの使用時にも当然問題になります.
DataRecord[A]というパラメタ化したcase classにオブジェクトと要素名を両方保存することで解決しました.
case class DataRecord[A](key: String, value: A) {
def toXML(elementLabel: String,
scope: scala.xml.NamespaceBinding): scala.xml.Node = value match {
case x: DataModel => x.toXML(elementLabel, scope)
case _ => scala.xml.Elem(null, elementLabel, scala.xml.Null, scope,
scala.xml.Text(value.toString))
}
}
case classをパラメタ化することで,choiceラッパーもいらなくすることができました.
名前空間ごとに別のパッケージ内にクラスを生成できるようになりました.
-p:<namespaceURI>=<package> | --package:<namespaceURI>=<package>
specifies the target package for <namespaceURI>
例えばXML SignatureとSAML 2.0 assertionは別のパッケージの方がいいですね:
$ scalaxb -p:http://www.w3.org/2000/09/xmldsig#=org.w3.xmldsig -p:http://www.w3.org/2001/04/xmlenc#=org.w3.xmlenc -p:urn:oasis:names:tc:SAML:2.0:assertion=org.xml.saml2.assertion -p:urn:oasis:names:tc:SAML:2.0:metadata=org.xml.saml2.metadata saml-schema-metadata-2.0.xsd saml-schema-assertion-2.0.xsd xenc-schema.xsd xmldsig-core-schema.xsd
generated ./xmldsig-core-schema.scala
generated ./xenc-schema.scala
generated ./saml-schema-assertion-2.scala
generated ./saml-schema-metadata-2.scala
$ scalac xmldsig-core-schema.scala xenc-schema.scala saml-schema-assertion-2.scala saml-schema-metadata-2.scala Helper.scala
これは結構長い事欲しかった機能で,scoptにkeyValueOptを実装したのが一週間前なので,最低でも一週間ですね.これにより,Helper object は Helper.scala というファイル内のorg.scalaxb.rtというパッケージに移行します.
I’ve been working on Issue 1 submitted by Tsuresh Kumar for a while now. The idea is to compile the schema for SAML Metadata, which is saml-schema-metadata-2.0.xsd. I need to implement a few things so scalaxb can handle the schema.
The biggest issue is the handling of multiple schema documents. Real life schema like SAML Metadata defines the schema on top of existing schema documents/namespaces. The current implementation of scalaxb assumes that the given schema contains everything, and thus the namespace handling was sloppy. Now that it needs to handle multiple namespaces other than the targetNamespace and XSD namespace, I cleaned up the code to convert the prefixes into URI and keep them around in the ElemRef and AttributeRef object.