XML PlayFramework Templates
While some people hate Scala's XML, I haven't worked with it long enough to form an opinion. So today I decided that I'd dive into it by creating an application that wrapped some simple data and spat out some XML.
Since I'm mainly focusing on the XML side of things, I'd rather not deal with connecting to database's or anything like that. So our data will come from a simple companion object instead of a database.
** Tip: ** If you'd like to follow along in the code or make modifications yourself, simply clone the example repository here.
First off, we need to create the project structure:
mkdir app mkdir app/controllers mkdir app/views mkdir app/models mkdir conf touch conf/routes mkdir project touch project/plugins.sbt touch build.sbt
Next, setup the build file to name your project and enable the Play Plugin:
//build.sbt lazy val root = (project in file(".")).enablePlugins(PlayScala) name := "xml-example" version := "1.0" scalaVersion := "2.10.4"
In order for this to load when we run sbt
, we need to specify where the
PlayPlugin is coming from by editing the project/plugins.sbt file:
// The Typesafe repository resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/" // Use the Play sbt plugin for Play projects addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.3.8")
With that in place we can now run sbt
and get a pleasant error message
from play about having no routes setup. To remedy update your routes file:
GET / controllers.Example.index
In order for this to compile we need to have an Example
controller. So
create one of those in controllers/Example.scala:
package controllers import play.api._ import play.api.mvc._ object Example extends Controller { def index = Action { Ok(views.xml.index(models.TestInfo.getData)) } }
Now we'll get an error about the views file not being defined, and the models not being defined either. Let's create the models file first:
package models import play.api._ import play.api.mvc._ case class TestInfo(id: Int, name: String, days: List[String]) object TestInfo { def getData : List[TestInfo] = { List( TestInfo(1, "First", List("Monday","Tuesday")), TestInfo(2, "First", List("Wednesday","Thursday")), TestInfo(3, "First", List("Monday","Friday")), TestInfo(4, "First", List("Saturday","Sunday")), TestInfo(5, "First", List()), TestInfo(6, "First", List("Tuesday")) ) } }
We've defined the companion object that will provide us with test data, and a simple case class to make doing so easier. Normally we'd write up something here that would provide data from a backend, such as elastic search or MySQL, but today it's just test data. Using this model we can now write our app/views/index.scala.xml file:
@(nodeList : List[TestInfo]) <?xml version="1.0" encoding="UTF-8"?> <TestInfoList> @for(testInfo <- nodeList){ <TestInfo> <Id>@testInfo.id</Id> <Name>@testInfo.name</Name> <Days> @for(day <- testInfo.days){ <Day>@day</Day> } </Days> </TestInfo> } </TestInfoList>
You'll notice there isn't anything different about this file versus a regular
xml template except that the name of the file is index.scala.xml as oppose
to your usual index.scala.html. However, to illustrate a point, let's
refactor our view to abstract the TestInfo
view to it's own file:
mkdir app/views/common vi app/views/common/testinfo.scala.xml @(testInfo: models.TestInfo) <TestInfo> <Id>@testInfo.id</Id> <Name>@testInfo.name</Name> <Days> @for(day <- testInfo.days){ <Day>@day</Day> } </Days> </TestInfo>
With this in place we'll change the app/views/index.scala.xml file to call the new one:
@(nodeList : List[TestInfo]) <?xml version="1.0" encoding="UTF-8"?> <TestInfoList> @for(testInfo <- nodeList){ @common.testinfo(testInfo) } </TestInfoList>
Note that the directories under the views folder correspond to their
packages. For those in the root of views, their type (html,xml) will be the
package (look at the controller, see views.xml.index
?). For templates in
the subdirectories, the classes become views.<subdir>.fileName.
XML is a useful protocol, but in order to have standards between the producer and consumer we need to specify a schema file, or an XSL. These are pretty easy to understand just by reading them, here's public/testInfo.xsd:
<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'> <xs:element name="TestInfoList"> <xs:complexType> <xs:sequence> <xs:element ref="TestInfo" minOccurs='0' maxOccurs='unbounded'/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="TestInfo"> <xs:complexType> <xs:sequence> <xs:element ref="Id" minOccurs='1' maxOccurs='1'/> <xs:element ref="Name" minOccurs='1' maxOccurs='1'/> <xs:element ref="Days" minOccurs='1' maxOccurs='1'/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="Name" type='xs:string'/> <xs:element name="Id" type='xs:integer'/> <xs:element name="Days"> <xs:complexType> <xs:sequence> <xs:element ref="Day" minOccurs='0' maxOccurs='7'/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="Day" type='xs:string'/> </xs:schema>
This file says that the valid scheme is one which has a TestInfoList
node
that contains any number of TestInfo
elements. Each of these elements are
then defined in terms of their pieces, namely Id
, Name
, and Days
. As
you might expect, each of these elements are defined within the file. XSD
is powerful, being Turing complete, we could techically accomplish almost
anything in it we could do in other languages! However, for this post we're
just going to use it to validate our generated XML. Let's update the output
of our layout:
@(nodeList : List[TestInfo]) <?xml version="1.0" encoding="UTF-8"?> <TestInfoList xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://localhost:9000/testInfo.xsd" > @for(testInfo <- nodeList){ @common.testinfo(testInfo) } </TestInfoList>
I'm assuming that you're running play on the default server port of 9000
in the example above. To make the /testInfo.xsd
route work we need to
update the routes themselves in conf/routes:
GET / controllers.Example.index GET /testInfo.xsd controllers.Assets.at(path:String = "/public/", file:String = "testInfo.xsd")
With those two things in place, any client will be able to validate your xml using the provided schema. If you want to check it out yourself, try this on the command line:
curl http://localhost:9000/ > tmp.xml xmllint --schema public/testInfo.xsd tmp.xml
And you should see a success method.
This post went over the bare neccesities of getting a Play application up from scratch and serving validatable XML. By using templates for pieces of your XML you can modularize your view code and make your life easier later on. Hopefully after reading this post you realize how easy it is to create XML views with Play. Have fun and happy coding!