ラウンド・トリップ

ラウンド・トリップ(往復券)の実装を始めています.つまり,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>

DataReferenceKeyReferenceの両要素とも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ラッパーもいらなくすることができました.

object Element1Option {  
  def fromXML: PartialFunction[scala.xml.NodeSeq, org.scalaxb.rt.DataRecord[Any]] = {
    case x: scala.xml.Elem if x.label == "Choice1" =>
      org.scalaxb.rt.DataRecord("Choice1", Choice1.fromXML(x))
    case x: scala.xml.Elem if x.label == "Choice2" =>
      org.scalaxb.rt.DataRecord("Choice2", x.text.toInt)
  }
}

Choice1の場合はChoice1.fromXML()にパースを委任して,Choice2の場合は中のテキストをIntに変換してDataRecordに入れています.scalaは自動的にDataRecord[Int]だと判断したみたいですね.

今後の予定としては,このジェネリックなコンテナを使って現在切り捨てている<any>をちゃんと変換することです.おそらくscala.xml.Elemをそのままの形でコレクションの中に残しておくような形になると思います.一旦ラウンド・トリップ機能を提供すると,ユーザは途中で荷物が無くならないことを期待すると思うので.