/xsbt-web-plugin

An sbt plugin for building Scala Web applications

Primary LanguageScalaOtherNOASSERTION

Build Status

Note: this is the readme for the upcoming 1.0 release. For the (stable) 0.9 release, see the 0.9 branch.

About

xsbt-web-plugin is an extension to sbt for building enterprise Web applications based on the Java J2EE Servlet specification.

xsbt-web-plugin supports both Scala and Java, and is best suited for projects that:

Requirements

  • sbt 0.13.x
  • Scala 2.10.x

Getting started

The quickest way to get started is to clone the xwp-template project, which sets up the necessary directories, files, and configuration for a basic xsbt-web-plugin project.

There are many examples in the form of tests in src/sbt-test.

How it works

xsbt-web-plugin consists of three modules: a webapp plugin, a war plugin, and a container plugin.

The webapp plugin is responsible for preparing a Servlet-based Web application as a directory, containing compiled project code, project resources, and a special webapp directory (which includes the web.xml configuration file, static HTML files, etc.).

The war plugin builds on the webapp plugin, adding a way to package the Web application directory as a .war file that can be published as an artifact, and deployed to a Servlet container.

The container plugin also builds on the webapp plugin, adding a way to launch a servlet container in a forked JVM to host the project as a Web application.

Put together, these compose xsb-web-plugin, and provide complete support for developing Servlet-based Web applications in Scala (and Java).

Quick reference

First, add xsbt-web-plugin:

project/plugins.sbt:

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.0.0-M4")

Then choose either Jetty or Tomcat with default setings:

build.sbt:

jetty()

build.sbt:

tomcat()

Start (or restart) the container with container:start:

sbt console:

> container:start

Stop the container with container:stop:

sbt console:

> container:stop

Build a .war file with package:

sbt console:

> package

Configuration and usage

Triggered (re)launch

sbt console:

> ~container:start

This starts the container, then monitors the sources, resources, and webapp directories for changes, which triggers a container restart.

Configure Jetty to run on port 9090

build.sbt:

jetty(port = 9090)

Configure Tomcat to run on port 9090

build.sbt:

tomcat(port = 9090)

Configure Jetty with jetty.xml

build.sbt:

jetty(config = "etc/jetty.xml")

The config path can be either absolute or relative to the project directory.

Depend on libraries in a multi-project build

build.sbt:

lazy val root = (project in file(".")) aggregate(mylib1, mylib2, mywebapp)

lazy val mylib1 = project

lazy val mylib2 = project

lazy val mywebapp = project dependsOn (mylib1, mylib2)

Add an additional source directory

build.sbt:

// add <project>/src/main/extra as an additional source directory
unmanagedSourceDirectories in Compile <+= (sourceDirectory in Compile)(_ / "extra")

Scala files in the extra source directory are compiled, and bundled in the project artifact .jar file.

Add an additional resource directory

build.sbt:

// add <project>/src/main/extra as an additional resource directory
unmanagedResourceDirectories in Compile <+= (sourceDirectory in Compile)(_ / "extra")

Files in the extra resource directory are not compiled, and are bundled directly in the project artifact .jar file.

Change the default Web application resources directory

build.sbt:

// set <project>/src/main/WebContent as the webapp resources directory
webappSrc in webapp <<= (sourceDirectory in Compile) map  { _ / "WebContent" }

The Web application resources directory is where static Web content (including .html, .css, and .js files, the web.xml container configuration file, etc. By default, this is kept in /src/main/webapp.

Change the default Web application destination directory

build.sbt:

// set <project>/target/WebContent as the webapp destination directory
webappDest in webapp <<= target map  { _ / "WebContent" }

The Web application destination directory is where the static Web content, compiled Scala classes, library .jar files, etc. are placed. By default, they go to /target/webapp.

Modify the contents of the prepared Web application

After the /target/webapp directory is prepared, it can be modified with an arbitrary File => Unit function.

For example, here's how to minify JavaScript files using YUI Compressor:

project/plugins.sbt:

libraryDependencies += "com.yahoo.platform.yui" % "yuicompressor" % "2.4.7" intransitive()

build.sbt:

// minify the JavaScript file script.js to script-min.js
postProcess in webapp := {
  webappDir =>
    import java.io.File
    import com.yahoo.platform.yui.compressor.YUICompressor
    val src  = new File(webappDir, "script.js")
    val dest = new File(webappDir, "script-min.js")
    YUICompressor.main(Array(src.getPath, "-o", dest.getPath))
}

Use WEB-INF/classes instead of WEB-INF/lib

By default, project classes and resources are packaged in the default .jar file artifact, which is copied to WEB-INF/lib. This file can optionally be ignored, and the project classes and resources copied directly to WEB-INF/classes.

build.sbt:

webInfClasses in webapp := true

Prepare the Web application for execution and deployment

For situations when the prepared /target/webapp directory is needed, but the packaged .war file isn't.

sbt console:

webapp:prepare

Use a cusom webapp runner

By default, either Jetty's jetty-runner or Tomcat's webapp-runner will be used to launch the container under container:start.

To use a custom runner, use runnerContainer with warSettings and webappSettings:

build.sbt:

runnerContainer(
  libs = Seq(
      "org.eclipse.jetty" %  "jetty-webapp" % "9.1.0.v20131115" % "container"
    , "org.eclipse.jetty" %  "jetty-plus"   % "9.1.0.v20131115" % "container"
    , "test"              %% "runner"       % "0.1.0-SNAPSHOT"  % "container"
  ),
  args = Seq("runner.Run", "8080")
) ++ warSettings ++ webappSettings

Here, libs includes the ModuleIDs of libraries needed to make our runner, which is invoked by calling the main method of runner.Run with a single argument to specify the server port.

Attaching a Java agent

build.sbt:

javaOptions += "-agentpath:/path/to/libyjpagent.jnilib"

Adding manifest attributes

By default, the .war file includes the same manifest attributes as the project's artifact:

  • Implementation-Vendor
  • Implementation-Title
  • Implementation-Version
  • Implementation-Vendor-Id
  • Specification-Vendor
  • Specification-Title
  • Specification-Version

These can be changed in both artifacts:

build.sbt:

packageOptions in (Compile, packageBin) +=
    Package.ManifestAttributes( java.util.jar.Attributes.Name.SEALED -> "true" )

Or in just the .war file:

build.sbt:

packageOptions in packageWar +=
  Package.ManifestAttributes( java.util.jar.Attributes.Name.SEALED -> "true" )

Starting from scratch

Create a new empty project:

mkdir myproject
cd myproject

Set up the project structure:

mkdir project
mkdir -p src/main/scala
mkdir -p src/main/webapp/WEB-INF

Configure sbt:

project/build.properties:

sbt.version=0.13.5

project/plugins.sbt:

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.0.0-M4")

build.sbt:

name := "myproject"

organization := "myorganization"

version := "0.1.0-SNAPSHOT"

scalaVersion := "2.10.4"

jetty()

libraryDependencies += "javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided"

Add a servlet:

src/main/scala/servlets.scala:

package servlets

import scala.xml.NodeSeq
import scala.xml.PrettyPrinter
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class MyServlet extends HttpServlet {

  override def doGet(request: HttpServletRequest, response: HttpServletResponse) {

    response.setContentType("text/html")
    response.setCharacterEncoding("UTF-8")

    val responseBody: NodeSeq =
      <html>
        <body>
          <h1>Hello, world!</h1>
        </body>
      </html>

    val printer = new PrettyPrinter(80, 2)

    response.getWriter.write(printer.formatNodes(responseBody))

  }

}

src/main/webapp/WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
	      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
	      http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	      version="3.0">

  <servlet>
    <servlet-name>my servlet</servlet-name>
    <servlet-class>servlets.MyServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>my servlet</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

</web-app>

Tips and tricks

To disable publishing of the .war file, add the setting:

packagedArtifacts <<= packagedArtifacts map { as => as.filter(_._1.`type` != "war") }

Note that package can still be used to create the .war file under the project target/ directory.

To disable publishing of the project's .jar file, add the setting:

publishArtifact in (Compile, packageBin) := false

Deployment

Tomcat

On Ubuntu, install both tomcat7 and tomcat7-admin:

sudo apt-get install tomcat7 tomcat7-admin

Create a Tomcat user with the role manager-script in /etc/tomcat7/tomcat-users.xml:

<user username="manager" password="secret" roles="manager-script" />

Restart tomcat:

sudo service tomcat7 restart

Now a WAR file can be deployed using the Manager deploy command:

curl --upload-file myapp.war "http://manager:secret@myhost:8080/manager/text/deploy?path=/myapp&update=true"

The application will be available at myhost:8080/myapp.

Learn more about Manager commands here.

Heroku

  1. Install the Heroku Toolbelt

  2. Install the heroku-deploy command line plugin:

heroku plugins:install https://github.com/heroku/heroku-deploy
  1. Create a WAR file:
sbt package

4 Deploy the WAR file:

heroku deploy:war --war <path_to_war_file> --app <app_name>

See devcenter.heroku.com for more information.

Google App Engine

See developers.google.com for more information.