Changing Scalatra and sbt default directories

Conventions are not always good. I recently started a new internal web project at Codurance and I decided to use Scala. As I'm not a fan of big frameworks, I chose Scalatra as a micro web framework.

The first challenge was that I wanted to organise my application with a different directory structure. By default, sbt and Scalatra use the same convention used by maven:

> /src/main/scala       // source code
> /src/main/resources   // production resources
> /scr/test/scala       // tests
> /scr/test/resouces    // test resources

For the past few years I've been experimenting with different directory structure for my projects. I want my directories to be more meaningful when it comes to explain the business domain. The new directory structure is part of what I call Interaction Driven Design (IDD) and a full talk on it can be found in our videos area. I give a lot of details about the rational behind the new directory structure on that talk.

The directory structure I would like to use for this new project is:

> /src/core/scala          // source code for my core domain
> /src/core-test/scala     // tests for my core domain

> /src/data/resources      // resources for data migration and test data
> /src/data/scala          // code for data migration and test data

> /src/web/resources       // delivery mechanism resources
> /src/web/scala           // delivery mechanism code (controllers, API, etc)
> /src/web/webapp          // web files (WEB-INF folder, css, javascript, Jade templates, etc)
> /src/web-test/scala      // tests for my delivery mechanism

Once again, the directory structure above will make more sense if you watch the Interaction Driven Design (IDD) talk.

The biggest challenge was to rename the default directory main to web. That broke the whole world. Here are the changes I had to make to fix it all:

build.sbt

unmanagedSourceDirectories in Compile := Seq((baseDirectory.value / "src/core/scala"),
                                             (baseDirectory.value / "src/data/scala"),
                                             (baseDirectory.value / "src/web/scala"))

unmanagedResourceDirectories in Compile += baseDirectory.value / "src/data/resources"

unmanagedSourceDirectories in Test := Seq((baseDirectory.value / "src/core-test/scala"),
                                          (baseDirectory.value / "src/web-test/scala"))

webappSrc in webapp <<= (baseDirectory in Compile) map { _ / "src/web/webapp" }

webappDest in webapp <<= (baseDirectory in Compile) map { _ / "src/web/webapp" }

The last two lines webappSrc and webappDest were needed because I also use a class that starts Jetty by hand where I hook the Scalatra listener.

JettyLauncher.scala

import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.DefaultServlet
import org.eclipse.jetty.webapp.WebAppContext
import org.scalatra.servlet.ScalatraListener

object JettyLauncher {

    def main(args: Array[String]) {
        val port = if(System.getenv("PORT") != null) System.getenv("PORT").toInt else 8089

        val server = new Server(port)
        val context = new WebAppContext()
        context.setClassLoader(JettyLauncher.getClass.getClassLoader)
        context setContextPath "/"
        context.setResourceBase("src/web/webapp")
        context.addEventListener(new ScalatraListener)
        context.addServlet(classOf[DefaultServlet], "/")

        server.setHandler(context)

        server.start
        server.join
    }
}

When executing this class, the ScalatraBootstrap could not be found and that's why I had to add the following line to my JettyLauncher:

> context.setClassLoader(JettyLauncher.getClass.getClassLoader)

Scalatra relies on the default directory main to find ScalatraBootstrap and this is how I managed to make sure the ScalatraBootstrap could be found.

Note that I also had to change the resource base, pointing to the web folder instead of main:

> context.setResourceBase("src/web/webapp")

As I use Jade templates via Scalate, I had to change the Scalate template configuration on build.scala.

build.scala

object MonitorBuild extends Build {
    val Organization = "com.codurance"
    val Name = "monitor"
    val Version = "0.1.0-SNAPSHOT"
    val ScalaVersion = "2.11.6"
    val ScalatraVersion = "2.4.0.RC1"

    lazy val project = Project(
        "monitor",
        file("."),
        settings = ScalatraPlugin.scalatraSettings ++ scalateSettings ++ Seq( // dependencies and some other stuff here scalateTemplateConfig in Compile <<= (sourceDirectory in Compile) { base =>
                Seq( TemplateConfig( new RichFile(new File("src")) / "web" / "webapp" / "WEB-INF" / "templates",
                        Seq.empty, 
                        Seq( Binding("context", "_root_.org.scalatra.scalate.ScalatraRenderContext", importMembers = true, isImplicit = true)
                        ), 
                        Some("templates")
                    )
                )
            }
        )
    )
}

The important line above is:

> new RichFile(new File("src")) / "web" / "webapp" / "WEB-INF" / "templates"

Which makes Scalate find the templates in the web directory instead of main.

Make sure you have these lines in the plugins.sbt

> addSbtPlugin("com.mojolly.scalate" % "xsbt-scalate-generator" % "0.5.0")
>
> addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.4.0")

I didn't need to change anything on my web.xml and ScalatraBootstrap.

The configuration described above allows me to run the application via

> ./sbt container:start

that is how I normally run the application locally and also allows me to create a fat jar file and execute the JettyLauncher class that is how I run in production:

> java -cp <myapplication>.jar com.codurance.JettyLauncher

The fat jar file is created via:

> ./sbt assembly 

This is how the collapsed directory structure looks on IntelliJ IDEA:

and this is how it looks when expanded:

Although it took me a while to figure all this out, I'm happy to be able to structure my project the way it makes sense to us.