9

I implemented a Scalatra servlet and now want to create an executable jar, just like described in this tutorial: http://www.scalatra.org/2.2/guides/deployment/standalone.html

I use IntelliJ IDEA with the Scala plugin for development and sbt to build and run my servlet (I used sbt-idea to generate the project files). My problem is that the jetty packages that the JettyLauncher in the tutorial uses cannot be found when I try to compile my project.

UPDATE: Using Matt's answer I was able to compile and run the JettyLauncher. However, I still have problems with sbt-assembly (https://github.com/sbt/sbt-assembly). I followed the instruction in the readme, but I get the following error when trying to execute the assembly task:

[error] Not a valid command: assembly
[error] No such setting/task
[error] assembly
[error]         ^  

UPDATE 2: Thanks to Matt I now have a working build.scala and I can generate a executable jar using the assembly task. However, sbt-assembly does not add the content of /src/main/webapp to the jar. I use this folder to store my HTML, CSS, and JavaScript files. If Scalatra can't match a route, it serves these files, which works when running the servlet using container:start. Additionally, I store some files that the server needs in /src/main/webapp/WEB-INF. These files are also not added to the jar.

My build.scala looks like this:

import sbt._
import Keys._
import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._
import com.mojolly.scalate.ScalatePlugin._
import ScalateKeys._
import sbtassembly.Plugin._
import AssemblyKeys._

object SketchlinkBuild extends Build {
  val Organization = "de.foobar"
  val Name = "Foobar"
  val Version = "0.1"
  val ScalaVersion = "2.10.0"
  val ScalatraVersion = "2.2.0"

  lazy val project = Project (
    "foobar",
    file("."),
    settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ scalateSettings ++ assemblySettings ++ Seq(
      organization := Organization,
      name := Name,
      version := Version,
      scalaVersion := ScalaVersion,
      resolvers += Classpaths.typesafeReleases,
      libraryDependencies ++= Seq(
            "org.scalatra" %% "scalatra" % ScalatraVersion,
            "org.scalatra" %% "scalatra-scalate" % ScalatraVersion,
            "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
            "ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
            "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "compile;container",
            "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "compile;container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")),
            /* Apache commons libraries */
            "commons-codec" % "commons-codec" % "1.7", 
            "commons-io" % "commons-io" % "2.4",
            /* JSON support */
            "org.scalatra" %% "scalatra-json" % "2.2.1",
            "org.json4s"   %% "json4s-jackson" % "3.2.4",
            /* thumbnail library */
            "net.coobird" % "thumbnailator" % "0.4.3"
     ),
     // ignore about.html in jars (needed for sbt-assembly)
     mergeStrategy in assembly <<= (mergeStrategy in assembly) { (old) => {
       case "about.html" => MergeStrategy.discard
       case x => old(x) }
     },
     scalateTemplateConfig in Compile <<= (sourceDirectory in Compile){ base =>
        Seq(
          TemplateConfig(
            base / "webapp" / "WEB-INF" / "templates",
            Seq.empty,  /* default imports should be added here */
            Seq(
              Binding("context", "_root_.org.scalatra.scalate.ScalatraRenderContext", importMembers = true, isImplicit = true)
            ),  /* add extra bindings here */
            Some("templates")
          )
        )
      }
    )
  )
}

Thanks in advance!

Eugene Yokota
  • 94,654
  • 45
  • 215
  • 319
sbaltes
  • 489
  • 1
  • 9
  • 17

3 Answers3

15

There are two options of standalone deployment currently:

  1. Single .jar using sbt-assembly which contains runtime and webapp resources. Loading resources from the .jar file is quite slow in my experience.
  2. Distribution .zip file using scalatra-sbt plugin, contains a start shell script, the runtime resources and the webapp resources in folders.

1. Standalone JAR

For a standalone .jar file using sbt-assembly you need to add the plugin first to project/build.sbt:

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.9.0")

Then you need to modify the project build, e.g. project/build.scala. Import the plugin's settings and keys:

import sbtassembly.Plugin._
import sbtassembly.Plugin.AssemblyKeys._

With that you can create settings for the sbt-assembly plugin:

// settings for sbt-assembly plugin
val myAssemblySettings = assemblySettings ++ Seq(

  // handle conflicts during assembly task
  mergeStrategy in assembly <<= (mergeStrategy in assembly) {
    (old) => {
      case "about.html" => MergeStrategy.first
      case x => old(x)
    }
  },

  // copy web resources to /webapp folder
  resourceGenerators in Compile <+= (resourceManaged, baseDirectory) map {
    (managedBase, base) =>
      val webappBase = base / "src" / "main" / "webapp"
      for {
        (from, to) <- webappBase ** "*" x rebase(webappBase, managedBase / "main" / "webapp")
      } yield {
        Sync.copy(from, to)
        to
      }
  }
)

The first defines a merge strategy, the last one copies the static web resources from src/main/webapp to <resourceManaged>/main/webapp. They will be included in the final .jar in a sub-folder /webapp.

Include the settings in your project:

lazy val project = Project("myProj", file(".")).settings(mySettings: _*).settings(myAssemblySettings:_*)

Now the launcher needs to be created. Note how the resource base is set:

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

object JettyMain {

  def run = {
    val server = new Server
    val connector = new SelectChannelConnector
    connector.setPort(8080)
    server.addConnector(connector)

    val context = new WebAppContext
    context.setContextPath("/")

    val resourceBase = getClass.getClassLoader.getResource("webapp").toExternalForm
    context.setResourceBase(resourceBase)
    context.setEventListeners(Array(new ScalatraListener))
    server.setHandler(context)

    server.start
    server.join
  }
}

2. .zip Distribution using scalatra-sbt Plugin

You need to add those imports to your SBT build.scala:

import org.scalatra.sbt.DistPlugin._
import org.scalatra.sbt.DistPlugin.DistKeys._

Then you need to add the plugin's settings to your project. The settings are in DistPlugin.distSettings.

You can also customize your distribution and add custom memory settings, exports and command line options. Note that those are all optional:

val myDistSettings = DistPlugin.distSettings ++ Seq(
  mainClass in Dist := Some("ScalatraLauncher"),
  memSetting in Dist := "2g",
  permGenSetting in Dist := "256m",
  envExports in Dist := Seq("LC_CTYPE=en_US.UTF-8", "LC_ALL=en_US.utf-8"),
  javaOptions in Dist ++= Seq("-Xss4m", "-Dfile.encoding=UTF-8")
)

On the SBT prompt you can then type dist. The .zip file will be in the target folder.

Stefan Ollinger
  • 1,577
  • 9
  • 16
  • Thanks! I will check this solution later and set it as accepted answer if it works. – sbaltes Sep 03 '13 at 10:56
  • I tried this and I got 'Error: Main method not found in class loadtest.jetty.Jetty, please define the main method as: public static void main(String[] args)' Please change your Jetty Launcher example because it doesn't work? I also had a lot of trouble getting SBT assembly to find the main class. I think this is because of the equal sign in run although the Scalatra documentation does this as well for the main function? – Mark Butler Aug 07 '14 at 22:28
  • This answer is slightly out of date: SelectChannelConnector is no longer available in Jetty v9. Also for the second option you need to mention that developers must create a ScalatraLauncher and ensure jetty deps are now in the compile scope. – Ricardo Gladwell Aug 13 '14 at 15:12
4

I recently ran into trouble doing this.

First, you need to make sure that jetty is available at compile time. These two lines:

"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" %     "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")),

Need to have compile in them:

"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "compile;container",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "compile;container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))

Second, from your description it sounds like sbt-assembly is not configured correctly. You need to remove (comment out) these lines:

lazy val buildSettings = Defaults.defaultSettings ++ Seq(
  version := "0.1",
  organization := "de.foobar",
  scalaVersion := "2.10.1"
)

lazy val app = Project("app", file("app"),
  settings = buildSettings ++ assemblySettings) settings(
    // your settings here
)

You will need to add ++ assemblySettings to your foobar project immediately after scalateSettings. Your plugins.sbt file also needs to contain the following line in it:

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.9.0")

For reference, I recommend against using sbt-assembly because you will most likely run into dependency conflicts that will need to be resolved with a merge strategy. Instead I suggest you use a task that collects your dependencies into a directory (examples here and here). And then add them to the java classpath using java -cp /lib/* ....

Third, be wary of the Jetty project in Scalatra's GitHub. I used:

import java.net.InetSocketAddress
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.DefaultServlet

import org.scalatra.servlet.ScalatraListener
import org.eclipse.jetty.webapp.WebAppContext

object Jetty {
  def main(args: Array[String]) = {
    val socketAddress = new InetSocketAddress(8080)
    val server = new Server(socketAddress)
    val context = new WebAppContext()
    context.setContextPath("/")
    context.setResourceBase("src/main/webapp")
    context.addEventListener(new ScalatraListener)
    context.addServlet(classOf[DefaultServlet], "/")
    server.setHandler(context)
    server.start()
    server.join()
  }
}

Finally, it might be worth double checking your ScalatraBootstrap is in the usual place.

Hope that helps. If not I can post my entire build.scala for you.

Community
  • 1
  • 1
Matt Roberts
  • 1,107
  • 10
  • 15
  • Thanks for your answer. After adding the compile option, I was able to compile and run the jetty launcher. But I still have problems with sbt-assembly. I'll update my question. – sbaltes Jun 01 '13 at 10:09
  • No problem. I've edited and added some information on sbt-assembly. – Matt Roberts Jun 02 '13 at 11:43
  • Thanks again for your updated answer. I had to define a merge strategy to ignore the about.html files in the jars, but thanks to your help I now have a executable jar. However, sbt-assembly does not add the contents of /src/main/webapp to the jar. I'll update my question (once again). – sbaltes Jun 03 '13 at 11:57
1

For an up to date answer, please refer to these two files (Credits go to Scalatra in Action book):

https://github.com/scalatra/scalatra-in-action/blob/master/chapter09-standalone/src/main/scala/ScalatraLauncher.scala

and

https://github.com/scalatra/scalatra-in-action/blob/master/chapter09-standalone/project/Build.scala

Ali Salehi
  • 6,899
  • 11
  • 49
  • 75