-
Notifications
You must be signed in to change notification settings - Fork 18
XML Tutorial
We use the same design as Play2 Json serialization/deserialization based on implicit typeclasses. This mechanism is very clean, robust and generic. Please note that implicits are NOT used as implicit CONVERSIONS which are often tricky and sometimes dangerous!
val fooXml = <foo>
<id>1234</id>
<name>brutus</name>
<age>23</age>
</foo>
case class Foo(id: Long, name: String, age: Option[Int])
Foo(id=1234L, name="brutus", age=Some(23))
Please note:
- case class is a sample but the mechanism also works with any structure in Scala such as tuples
-
agefield isOption[Int]meaning it might not appear in the XML (Nonein this case)
import play2.tools.xml._
import play2.tools.xml.DefaultImplicits._
[...]
val foo = EXML.fromXML[Foo](fooXml)
assert(foo == Foo(1234L, "brutus", Some(23)))
val fooXml2 = EXML.toXML(foo)
assert(fooXml2 == fooXml)
As you may imagine, this is not so simple as fromXML/toXML signatures are the following:
object EXML {
def toXML[T](t: T, base: xml.NodeSeq = xml.NodeSeq.Empty)(implicit w: XMLWriter[T]): xml.NodeSeq
def fromXML[T](x: xml.NodeSeq)(implicit r: XMLReader[T]): Option[T]
}
You can see the implicit typeclasses XMLReader/XMLWriter which define the mapper to/from XML to your case class.
So in order EXML.fromXML/EXML.toXML to work properly, you should define an implicit XML reader/writer for your specific structure in your scope.
It can be done at once by extending XMLFormatter[T]:
trait XMLFormatter[T] extends XMLReader[T] with XMLWriter[T] {
def read(x: xml.NodeSeq): Option[T]
def write(f: T, base: xml.NodeSeq): xml.NodeSeq
}
implicit object FooXMLF extends XMLFormatter[Foo] {
def read(x: xml.NodeSeq): Option[Foo] = {
for(
id <- EXML.fromXML[Long](x \ "id");
name <- EXML.fromXML[String](x \ "name");
age <- EXML.fromXML[Option[Int]](x \ "age")
) yield(Foo(id, name, age))
}
def write(f: Foo, base: xml.NodeSeq): xml.NodeSeq = {
<foo>
<id>{ f.id }</id>
<name>{ f.name }</name>
{ EXML.toXML(f.age, <age/>) }
</foo>
}
}
You may think this is a bit tedious to write but this is quite easy after a few tries and the most important:
This mechanism provides a very precise and simple control on what you want to do.
Please note:
-
the
writefunction uses Scala XML literals simply. -
the
implicitis important: you can declare it once in your scope and that's all. -
the
agefield inwriterequires a special syntax:{ EXML.toXML(f.age, <age/>) }means:<age/>is used as the base node and will generate following XML:- if age is
Some(23):<age>23</age> - if age is
None:<age xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" />(standards defines this but you can redefine it as you want)
- if age is
-
the
basefield inwritefunction can be used to pass a parent context node to the writer. It is helpful in case you need to add attributes to parent node for ex as in the case ofage: Option[Int]field.
But don't forget that Scala XML nodes are immutable and that you can just copy the nodes you pass to a function.
-
the for-comprehension is just a shortcut but you could write it using flatMap/map also: For ex:
def read(x: xml.NodeSeq): Option[Foo] = { EXML.fromXML[Long](x \ "id").flatMap{ id => EXML.fromXML[String](x \ "name").flatMap { name => EXML.fromXML[Int](x \ "age").flatMap{ age => Foo(id, name, age) } } } }
import play2.tools.xml._
import play2.tools.xml.DefaultImplicits._
implicit object FooXMLF extends XMLFormatter[Foo] {
def read(x: xml.NodeSeq): Option[Foo] = {
for(
id <- EXML.fromXML[Long](x \ "id");
name <- EXML.fromXML[String](x \ "name");
age <- EXML.fromXML[Option[Int]](x \ "age");
) yield(Foo(id, name, age))
}
def write(f: Foo, base: xml.NodeSeq): xml.NodeSeq = {
<foo>
<id>{ f.id }</id>
<name>{ f.name }</name>
{ EXML.toXML(f.age, <age/>) }
</foo>
}
}
val foo = EXML.fromXML[Foo](fooXml)
assert(foo == Foo(1234L, "albert", 23)
val fooXml = EXML.toXML(foo)
assert(fooXml == fooXml)