混在内容,再び
混在内容のサポートを前に始めましたが,おさらいをしておくと,<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生成の方法は事前に出荷できますが,ユーザ定義型もサポートする必要があります.型クラスで実装してみることにしました:
trait XMLWriter[A] { implicit val ev = this def toXML(__obj: A, __namespace: Option[String], __elementLabel: Option[String], __scope: NamespaceBinding): NodeSeq }
全てのコンパニオン・オブジェクトはすでに toXML
を実装しているので,XMLWriter[A]
を継承するだけです.一度 DataRecord[Any]
に格納してしまって Any
になってしまったオブジェクトから XMLWriter[A]
を抜き出す方法が分からなかったので,XMLWriter[A]
を DataRecord[Any]
に保存することにしました.この方法の問題は DataRecord[A]
における値の型の A
によって決まりきった値のパラメータが余計に増えることです.String
の場合は何らかの __StringXMLWriter
になるし,Address
の場合は常に Address
となります.また,余計なパラメータはパターン・マッチの邪魔にもなります.これをどのように回避できるでしょうか.
まず,object DataRecord
下に def dataRecord
という名前でコンストラクタ・ヘルパを定義します.これは最初の三つのパラメータは明示的に取った後,XMLWriter[A]
を context-bound 文法によって暗黙に取ります:
def dataRecord[A:XMLWriter](namespace: Option[String], key: Option[String], value: A): DataRecord[A] = DataRecord(namespace, key, value, implicitly[XMLWriter[A]])
この時点で,scalaxb で使われる組み込み型と XML ノードに関して暗黙の値を提供する必要があります:
object XMLWriter { implicit object __NodeXMLWriter extends XMLWriter[Node] { def toXML(__obj: Node, __namespace: Option[String], __elementLabel: Option[String], __scope: NamespaceBinding): NodeSeq = __obj } implicit object __StringXMLWriter extends XMLWriter[String] { def toXML(__obj: String, __namespace: Option[String], __elementLabel: Option[String], __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq = Helper.stringToXML(__obj, __namespace, __elementLabel, __scope) } ... }
Scala の規格で興味深いのは暗黙のパラメータを探す場所です.Programming in Scala p.440-441:
さらに,一つの例外を覗いて,暗黙の型変換は一つの識別子としてスコープに入っていなければいけません.
「単一識別子」ルールに一つだけ例外があります.コンパイラは暗黙の値の定義を元の型および期待されている対象の型のコンパニオン・オブジェクトの中も探します.
XMLWriter[A]
の中に implicit val ev = this
の定義を入れておいたのに気付いたでしょうか:
trait XMLWriter[A] { implicit val ev = this def toXML(__obj: A, __namespace: Option[String], __elementLabel: Option[String], __scope: NamespaceBinding): NodeSeq }
Address
は XMLWriter[Address]
を継承するため,Address
オブジェクトも暗黙の値となります.
この新しい def dataRecord
は DataRecord
の生成の問題は解決しますが,パターン・マッチングに四つのパラメータがある問題が残っています.
パターン・マッチングはただ単に def unapply
を適用しただけの話なので,古い DataRecord
との互換性を保つために DataRecord
を元からの三つの値だけの trait として再定義することができます.object DataRecord
下に unapply
を以下のように定義します:
def unapply[A](record: DataRecord[A]): Option[(Option[String], Option[String], A)] = Some(record.namespace, record.key, record.value)
パターン・マッチングを模倣することができたので,どうせならオブジェクト生成も模倣してしまいましょう.def dataRecord
の代わりに def apply
と定義することで DataRecord
のコンストラクタを擬態することができます.XMLWriter[A]
を含めた実際の値の保持は private な case class を object DataRecord
内に隠し持ちます:
object DataRecord { private case class DataWriter[+A]( namespace: Option[String], key: Option[String], value: A, writer: XMLWriter[_]) extends DataRecord[A] def apply[A:XMLWriter](namespace: Option[String], key: Option[String], value: A): DataRecord[A] = DataWriter(namespace, key, value, implicitly[XMLWriter[A]]) def apply[A:XMLWriter](value: A): DataRecord[A] = apply(None, None, value) def unapply[A](record: DataRecord[A]): Option[(Option[String], Option[String], A)] = Some(record.namespace, record.key, record.value) def toXML[A](__obj: DataRecord[A], __namespace: Option[String], __elementLabel: Option[String], __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq = __obj match { case w: DataWriter[_] => w.writer.asInstanceOf[XMLWriter[A]].toXML(__obj.value, __namespace, __elementLabel, __scope) case _ => error("unknown DataRecord.") } }
これで後方互換性があり,かつ型独自の XMLアウトプットができる DataRecord
ができました.