Tuesday, January 25, 2011

A Quick WebApp with Scala, MongoDB, Scalatra and Casbah

Here's a nice way to get a web service up and running quickly using Scala, Scalatra, SBT and MongoDB. I'm going to assume you have Scala installed but nothing beyond that. Feel free to skip the episodes you've already seen.

Install SBT
Simple Build Tool (SBT) is a build tool for Scala, and it's just super. To install it, download the jar from here: http://code.google.com/p/simple-build-tool/downloads/list

Put that jar someplace and the make a little script along the lines of this:
java -Xmx1512M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=512m -jar `dirname $0`/sbt-launch.jar "$@"
Save that alongside the jar you downloaded. Make it executable and add it to your PATH.

MongoDB
MongoDB ( http://www.mongodb.org ) is a document database, meaning it persists JSON-like documents of arbitrary structure. It's very fast and offers very flexible ad hoc querying. A MongoDB document looks like this:
{
_id : ObjectId("4d2167a24d2fa1a764a6f0fa"),
date : "Sun Jan 02 2011 22:07:30 GMT-0800 (PST)",
body : "My blog post!",
upvotes : 3,
downvotes : 0,
comments : [
{
author : "The Dude",
body : "The Dude abides"
},
{
author : "Walter",
body : "Over the line!"
}
]
}
You don't create joins in the database - each document is schema-less and unrelated to other documents, even those in its collection (sort of like a SQL table in MongoDB parlance), But you can do cool stuff like index on those nested comments. Here's how to install it and get started:

Download the tarball for your architecture: http://www.mongodb.org/downloads

Expand it somewhere and add the bin directory to your PATH if you want to.

Run something like this to start the MongoDB server:
$MONGO_HOME/bin/mongod --dbpath=/path/to/place/to/save/mongodb/data
Connect to it and try it out like so:
$MONGO_HOME/bin/mongo
'mongo' is the MongoDB shell, which is a complete Javascript environment and connects to the local server on the default port. Create the example document above like this:
> use temp
switched to db temp
> db.demo.insert({date:new Date(), body:'My blog post!', upvotes: 3, dowvotes: 0, comments: [{author: 'The Dude', body: 'The Dude abides'},{author: 'Walter', body: 'Over the line!'}]})
> db.demo.find()
It's an excellent tool and a lot of fun. There are great docs at http://mongodb.org

Creating the SBT Project
Let's create an application. Make yourself a directory, navigate to it and run sbt. It will guide you through a wizard experience thusly:
$ sbt
Project does not exist, create new project? (y/N/s) y
Name: Demo
Organization: com.demo
Version [1.0]:
Scala version [2.7.7]: 2.8.1
sbt version [0.7.4]:
Getting Scala 2.7.7 ...
...more stuff...
[success] Successfully initialized directory structure.
...more stuff...
[info] Building project Demo 1.0 against Scala 2.8.1
[info] using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7
>
Leave that shell open to the sbt prompt. In your editor of choice, create a file in your project directory named
project/build/Project.scala
Add the following to it:
import sbt._
class build(info: ProjectInfo) extends DefaultWebProject(info) {
// scalatra
val sonatypeNexusSnapshots = "Sonatype Nexus Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
val sonatypeNexusReleases = "Sonatype Nexus Releases" at "https://oss.sonatype.org/content/repositories/releases"
val scalatra = "org.scalatra" %% "scalatra" % "2.0.0.M2"

// jetty
val jetty6 = "org.mortbay.jetty" % "jetty" % "6.1.22" % "test"
val servletApi = "org.mortbay.jetty" % "servlet-api" % "2.5-20081211" % "provided"

//casbah
val casbah = "com.mongodb.casbah" %% "casbah" % "2.0.1"
}
In your sbt shell, run
reload
and then
update
SBT will download dependencies ending with a message of success.

Add a Scalatra GET handler
Scalatra is refreshingly simple. To create a Scalatra servlet with a simple GET handler, using Scala's support for XML literals, create a file like src/main/scala/WebApp.scala with the following content:
import org.scalatra._

class WebApp extends ScalatraServlet {
get("/hello") {
<html><head><title>Hello!</title></head><body><h1>Hello</h1></body></html>
}
}
web.xml

One last bit of setup is to add the Scaltra servlet to the web.xml. Create a file named /src/main/webapp/WEB-INF/web.xml with the following content:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>

<servlet>
<servlet-name>WebApp</servlet-name>
<servlet-class>WebApp</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>WebApp</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
Back at the SBT prompt you just need to
reload
and then do the following to start jetty and listen for source file changes:
jetty-run
~prepare-webapp
Open a browser to http://localhost:8080/hello and you should get a working Scalatra app. Change the message to something more interesting, like "Hello World" or "Goodnight Moon", save the source file, and SBT will recompile and redeploy. Just reload the web browser to see your changes.

Parameters

Add a path parameter to your GET call thusly:

get("/hello/:name") {
<html><head><title>Hello!</title></head><body><h1>Hello {params("name")}!</h1></body></html>
}
Save the file, browse to http://localhost:8080/hello/handsome

Oh yeah!

A Form

Add a form like this:
 get("/msgs") {
<body>
<form method="POST" action="/msgs">
Author: <input type="text" name="author"/><br/>
Message: <input type="text" name="msg"/><br/>
<input type="submit"/>
</form>
</body>
}
You can see the fruits of your labor at http://localhost:8080/msgs

Casbah

Casbah is the officially supported Scala driver for Mongo. It wraps the Java driver and makes accessing MongoDB straightforward using idiomatic Scala. http://api.mongodb.org/scala/casbah/2.0.2/index.html

You already have the driver dependency in your project, but you'll need a few new imports:
import com.mongodb._
import com.mongodb.casbah.Imports._
import scala.xml._
And a MongoDB connection that can be shared across the application:
val mongo = MongoConnection()
val coll = mongo("blog")("msgs")
Add a method to handle the form POST:

post("/msgs") {
val builder = MongoDBObject.newBuilder
builder += "author" -> params("author")
builder += "msg" -> params("msg")

coll += builder.result.asDBObject
redirect("/msgs")
}
Slick, right? That's all it takes to add a new document to the "msgs" collection in the "blog" db. You don't even need to create the db and collection first - just make sure MongoDB is running on the local host at the default port 27017.

Now you should adjust the form page to display the existing messages. This code treats iterates over the whole collection - in a real-world application you'd want paging, querying, filtering etc.

Add this code to your get() method just after the body tag:
 
<ul>
{for (l <- coll) yield <li>
From: {l.getOrElse("author", "???")} -
{l.getOrElse("msg", "???")}</li>}
</ul>

Once your app builds and you reload the page, your form should work to add new messages with authors and display them.

In Conclusion

I've found this stack a great way to whip up a quick example, prototype or test. You can use SBT to build a war (the 'package' command) and deploy it to any container you want.

I've barely scratched the surface of any of these bits, but hopefully piqued some interest in some cool tools. There are great docs and examples for all of these projects, so dig in and have fun!

Source code:

project/build/Project.scala:

import sbt._
class build(info: ProjectInfo) extends DefaultWebProject(info) {
// scalatra
val sonatypeNexusSnapshots = "Sonatype Nexus Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
val sonatypeNexusReleases = "Sonatype Nexus Releases" at "https://oss.sonatype.org/content/repositories/releases"
val scalatra = "org.scalatra" %% "scalatra" % "2.0.0.M2"

// jetty
val jetty6 = "org.mortbay.jetty" % "jetty" % "6.1.22" % "test" servletApi = "org.mortbay.jetty" % "servlet-api" % "2.5-20081211" % "provided"

//casbah
val casbah = "com.mongodb.casbah" %% "casbah" % "2.0.1"
}

src/main/scala/WebApp.scala:

import org.scalatra._
import com.mongodb.casbah.Imports._
import scala.xml._
import com.mongodb._

class WebApp extends ScalatraServlet {

val mongo = MongoConnection()
val coll = mongo("blog")("msgs")

get("/hello/:name") {
<html><head><title>Hello!</title></head><body><h1>Hello {params("name")}!</h1></body></html>
}

get("/msgs") {
<body>
<ul>
{for (l <- coll) yield <li>
From: {l.getOrElse("author", "???")} -
{l.getOrElse("msg", "???")}</li>}
</ul>
<form method="POST" action="/msgs">
Author: <input type="text" name="author"/><br/>
Message: <input type="text" name="msg"/><br/>
<input type="submit"/>
</form>
</body>
}

post("/msgs") {
val builder = MongoDBObject.newBuilder
builder += "author" -> params("author")
builder += "msg" -> params("msg")

coll += builder.result.asDBObject
redirect("/msgs")
}
}

4 comments:

  1. Very nice intro; well organized and easy to follow. Scalatra looks pretty neat for simple Web applications.

    ReplyDelete
  2. Error running Jetty: java.net.BindException: Address already in use

    ^ if you get that (referring to port 8080), add this line to the class in Project.scala:

    override val jettyPort = 9999

    See http://code.google.com/p/simple-build-tool/wiki/WebApplications for more about jetty configuration.

    ReplyDelete
  3. Great article, i'd always tip toed around trying a light weight web framework like sinatra/scalatra but i'll definitely be giving it a whirl now. VERY simple.

    ReplyDelete