Problem: You want to convert the data in a Scala class to its equivalent XML, or convert an XML representation of a class to an instance of the class.
Solution
There are two primary approaches to solving this problem:
- Convert the fields in your Scala class to and from XML using the techniques shown in previous recipes in this chapter, i.e., a “manual conversion” process.
- Use a library like XStream to assist in the conversion process, writing converters as necessary.
This solution demonstrates both approaches.
1) The manual conversion process
The following code has a toXml
method in the class to convert the fields in a class to XML, and a fromXml
method in the object to convert an XML literal to the fields in a class, using a manual process. This example uses a Stock
class, meant to hold data for an instance of a stock, such as Stock(“NFLX”,“Netflix”,165.50)
:
class Stock(var symbol: String, var businessName: String, var price: Double) { // (a) convert Stock fields to XML def toXml = { <stock> <symbol>{symbol}</symbol> <businessName>{businessName}</businessName> <price>{price}</price> </stock> } override def toString = s"symbol: $symbol, businessName: $businessName, price: $price" } object Stock { // (b) convert XML to a Stock def fromXml(node: scala.xml.Node):Stock = { val symbol = (node \ "symbol").text val businessName = (node \ "businessName").text val price = (node \ "price").text.toDouble new Stock(symbol, businessName, price) } }
The toXml
method uses the variable substitution techniques shown in Recipe 16.2 of the Scala Cookbook, and the fromXml
method uses XPath search techniques demonstrated in Recipes 16.4 through 16.6. The toXml
method is declared in the class because it needs to be unique to each instance, but the fromXml
method is declared in the object because it’s called as Stock.fromXml(theXml)
, like a static method in Java.
(Note: The businessName
field isn’t necessary here, but is kept to make sure the stock symbols are understood.)
These methods are demonstrated with the following driver class:
object TestToFromXml extends App { // (a) convert a Stock to its XML representation val aapl = new Stock("AAPL", "Apple", 600d) println(aapl.toXml) // (b) convert an XML representation to a Stock val googXml = <stock> <symbol>GOOG</symbol> <businessName>Google</businessName> <price>620.00</price> </stock> val goog = Stock.fromXml(googXml) println(goog) }
Running the TestToFromXml
object produces the following output:
<stock> <symbol>AAPL</symbol> <businessName>Apple</businessName> <price>600.0</price> </stock> symbol: GOOG, businessName: Google, price: 620.0
This process is straightforward, and uses techniques demonstrated in other recipes in this chapter. The only thing new in this recipe is using the toDouble
method in this line of code:
val price = (node \ "price").text.toDouble
You can use toDouble
and all the related to*
methods, because the text method returns a String
, which you can manipulate in all the usual ways.
Although writing code like this is a manual process, the code can easily be generated from the class specification or the database using a “CRUD generator.”
2) Using a tool like XStream
Another solution to this problem is to use a library such as XStream, a popular Java library for serializing XML. Like other Java libraries, it works with Scala, though you’ll need to provide a “converter” to handle special situations and collections.
For instance, you might start with the following simple class:
// a "first attempt" example (has a few problems) import scala.collection.mutable.ArrayBuffer import com.thoughtworks.xstream._ import com.thoughtworks.xstream.io.xml.DomDriver case class Topping (name: String) case class Pizza(crustSize: Int, crustType: String) { val toppings = ArrayBuffer[Topping]() def addTopping(t: Topping) { toppings += t } } object Test extends App { val p = Pizza(14, "Thin") p.addTopping(Topping("cheese")) p.addTopping(Topping("sausage")) val xstream = new XStream(new DomDriver) val xml = xstream.toXML(p) println(xml) }
After including the XStream jar in the project, running the Test
object results in the following output, which has a few issues:
<Pizza> <crustSize>14</crustSize> <crustType>Thin</crustType> <toppings> <initialSize>16</initialSize> <array> <Topping> <name>cheese</name> </Topping> <Topping> <name>sausage</name> </Topping> <null/> <null/> <null/> ... (this goes on until 14 null fields are printed) ... <null/> <null/> <null/> </array> <size0>2</size0> </toppings> </Pizza>
Issues in this output include:
- The “Pizza” and “Topping” tags are capitalized.
- All those
<null/>
tags generated from theArrayBuffer[Topping]
. - The extra
<array>
and<size>
tags.
To solve these problems, you need to create aliases to handle the capitalization problems, and write an XStream converter to handle the collection.
A converter class named net.mixedbits.tools.XStreamConversions provides a good start toward solving the collections problem. At the time of this writing it currently has one “import” bug that’s easily fixed, plus some deprecation issues.
After copying and pasting the XStreamConversions class into my project and fixing its import bug, the following program shows how to use it. In addition, the alias
method calls show to convert the strings “Pizza” and “Topping” to lowercase:
// improved example import scala.collection.mutable.ArrayBuffer import com.thoughtworks.xstream._ import com.thoughtworks.xstream.io.xml.DomDriver import com.thoughtworks.xstream.io.xml.StaxDriver // added import net.mixedbits.tools.XStreamConversions case class Topping (name: String) case class Pizza(crustSize: Int, crustType: String) { val toppings = ArrayBuffer[Topping]() def addTopping(t: Topping) { toppings += t } } object Test extends App { val p = Pizza(14, "Thin") p.addTopping(Topping("cheese")) p.addTopping(Topping("pepperoni")) // pass XStream into XStreamConversions val xstream = XStreamConversions(new XStream(new DomDriver())) // make Topping and Pizza lowercase xstream.alias("topping", classOf[Topping]) xstream.alias("pizza", classOf[Pizza]) val xml = xstream.toXML(p) println(xml) }
Running this code results in the following (improved) output:
<pizza> <crustSize>14</crustSize> <crustType>Thin</crustType> <toppings> <topping> <name>cheese</name> </topping> <topping> <name>pepperoni</name> </topping> </toppings> </pizza>
A library like XStream can really save time and effort if you need to serialize and deserialize a large number of classes.
See Also
- The XStream library is “a simple library to serialize objects to XML and back again”: http://xstream.codehaus.org
- Information about tweaking XStream output: http://xstream.codehaus.org/manual-tweaking-output.html
- Writing an XStream converter: http://xstream.codehaus.org/converter-tutorial.html