diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 000000000..7daf09946 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,30 @@ +# Credits + +## Committers + +- [Gabriele Renzi](http://www.riffraff.info/) +- [Alan Dipert](http://alan.dipert.org/) +- [Ross A. Baker](http://www.rossabaker.com/) +- [Hiram Chirino](http://hiramchirino.com/) +- [Ivan Porto Carrero](http://flanders.co.nz/) + +## Other contributors + +- [Luke Amdor](http://rubbish.io/) +- [JR Boyens](http://jrboyens.github.com/) +- [Tuomas Kareinen](http://www.iki.fi/kareinen/) +- [Miso Korkiakoski](http://github.com/mwing) +- [Yusuke Kuoka](http://d.hatena.ne.jp/mumoshu/) +- [Yung-Luen Lan](http://yllan.org/) +- [Paul Lambert](http://paulitex.com/) +- [Ted Nyman](http://github.com/tnm) +- [Erik Rozendaal](http://github.com/erikrozendaal) +- [Ivan Willig](http://github.com/iwillig) +- [Phil Wills](http://github.com/philwills) + +## Special thanks + +- [The Sinatra Project](http://www.sinatrarb.com/), whose excellent framework, + test suite, and documentation we've shamelessly copied. +- [Mark Harrah](http://github.com/harrah) for his support on the [sbt mailing + list](http://groups.google.com/group/simple-build-tool) diff --git a/README.markdown b/README.markdown index 107486785..36b8959a6 100644 --- a/README.markdown +++ b/README.markdown @@ -43,6 +43,10 @@ Note: if you keep getting frequent OutOfMemory errors from `sbt` you can try cha Note 2: if you already have a checkout, and after a `git pull` the build fails, try to explicitly run the `update` and `clean` sbt tasks before running `compile`. +### Alternative Maven quickstart. + +See the [simple-scalatra-archetype](http://github.com/Srirangan/simple-scalatra-archetype). + ## Community ### Mailing list @@ -521,6 +525,11 @@ Another difference is that ScalatraFilter matches routes relative to the WAR's c ## Migration Guide +### scalatra-2.0.0.M2 to scalatra-2.0.0.M3 + +Should be compatible. If it broke, please share your tale of woe on the +mailing list. + ### scalatra-2.0.0.M1 to scalatra-2.0.0.M2 1. Session has been retrofitted to a Map interface. `get` now returns an option instead of the value. @@ -539,22 +548,3 @@ Scalatra was renamed from Step to Scalatra to avoid a naming conflict with (an u - [SSGI](http://github.com/scalatra/ssgi): Work in progress. Will provide an abstraction layer allowing a future version of Scalatra to run on web servers other than Servlet containers. - [Bowler](http://bowlerframework.org): A RESTful, multi-channel ready web framework in Scala with a functional flavour, built on top of Scalatra and [Scalate](http://scalate.fusesource.org/). - -## Credits - -### Committers - -- [Gabriele Renzi](http://www.riffraff.info/), who started it all with his [blog posts](http://www.riffraff.info/tags/step) -- [Alan Dipert](http://alan.dipert.org/) -- [Ross A. Baker](http://www.rossabaker.com/) -- [Hiram Chirino](http://hiramchirino.com) -- [Ivan Porto Carrero](http://flanders.co.nz) - -### Other contributors - -- [The Sinatra Project](http://www.sinatrarb.com/), whose excellent framework, test suite, and documentation we've shamelessly copied. -- [Mark Harrah](http://github.com/harrah) for his support on the SBT mailing list. -- [Yusuke Kuoka](http://github.com/mumoshu) for adding sessions and header support -- [Miso Korkiakoski](http://github.com/mwing) for various patches. -- [Ivan Willig](http://github.com/iwillig) for his work on [Scalate](http://scalate.fusesource.org/) integration. -- [Phil Wills](http://github.com/philwills) for the path parser cleanup. diff --git a/example/src/main/scala/org/scalatra/CookiesExample.scala b/example/src/main/scala/org/scalatra/CookiesExample.scala new file mode 100644 index 000000000..c0f805948 --- /dev/null +++ b/example/src/main/scala/org/scalatra/CookiesExample.scala @@ -0,0 +1,14 @@ +package org.scalatra + +class CookiesExample extends ScalatraServlet with CookieSupport { + get("/cookies-example") { + val previous = cookies.get("counter") match { + case Some(v) => v.toInt + case None => 0 + } + cookies.update("counter", (previous+1).toString) +

+ Hi, you have been on this page {previous} times already +

+ } +} diff --git a/example/src/main/scala/org/scalatra/TemplateExample.scala b/example/src/main/scala/org/scalatra/TemplateExample.scala index 8926881d5..fd1b19d9b 100644 --- a/example/src/main/scala/org/scalatra/TemplateExample.scala +++ b/example/src/main/scala/org/scalatra/TemplateExample.scala @@ -33,7 +33,8 @@ class TemplateExample extends ScalatraServlet with UrlSupport /*with FileUploadS flash scope login logout - filter demo + filter example + cookies example chat demo diff --git a/example/src/main/webapp/WEB-INF/web.xml b/example/src/main/webapp/WEB-INF/web.xml index 44babef20..f21b8ba66 100644 --- a/example/src/main/webapp/WEB-INF/web.xml +++ b/example/src/main/webapp/WEB-INF/web.xml @@ -1,61 +1,65 @@ +PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" +"http://java.sun.com/j2ee/dtds/web-app_2_2.dtd"> TemplateExample org.scalatra.TemplateExample - - BasicAuthExample - org.scalatra.BasicAuthExample - - - SocketIOExample - org.scalatra.SocketIOExample - - flashPolicyServerHost - localhost - - - flashPolicyServerPort - 843 - - - flashPolicyDomain - localhost - - - flashPolicyPorts - 8080 - - - - ChatApplication - org.scalatra.ChatServlet - - flashPolicyServerHost - localhost - - - flashPolicyServerPort - 843 - - - flashPolicyDomain - localhost - - - flashPolicyPorts - 8080 - - + + BasicAuthExample + org.scalatra.BasicAuthExample + + + SocketIOExample + org.scalatra.SocketIOExample + + flashPolicyServerHost + localhost + + + flashPolicyServerPort + 843 + + + flashPolicyDomain + localhost + + + flashPolicyPorts + 8080 + + + + CookiesExample + org.scalatra.CookiesExample + + + ChatApplication + org.scalatra.ChatServlet + + flashPolicyServerHost + localhost + + + flashPolicyServerPort + 843 + + + flashPolicyDomain + localhost + + + flashPolicyPorts + 8080 + + - ChatApplication - /socket.io/* + ChatApplication + /socket.io/* TemplateExample @@ -69,6 +73,10 @@ SocketIOExample /echoserver/* + + CookiesExample + /cookies-example/* + default /images/* diff --git a/notes/2.0.0.M3.markdown b/notes/2.0.0.M3.markdown new file mode 100644 index 000000000..347a5a55a --- /dev/null +++ b/notes/2.0.0.M3.markdown @@ -0,0 +1,14 @@ +* [GH-16](http://github.com/scalatra/scalatra/issues/16): Dealing with multiple servlets/filters which have FileUploadSupport. +* [GH-18](http://github.com/scalatra/scalatra/issues/18): ScalatraTests doesn't track the session cookie correctly across more than 2 requests +* Generate CSRF token once per session instead of once per request. +* [GH-19](http://github.com/scalatra/scalatra/issues/19): Render a File when returned by an action +* [GH-20](http://github.com/scalatra/scalatra/issues/20): Add support for the HTTP HEAD method. ScalatraTests support still pending. +* Upgrade to Jetty 7. +* Concurrency support for dynamic route addition and deletion. +* [GH-25](http://github.com/scalatra/scalatra/issues/25): Decode request body with content encoding +* New socketio module. _API subject to change before 2.0.0_ +* [GH-26](http://github.com/scalatra/scalatra/issues/26): Support options request +* Support for Rails-style path patterns. +* [GH-29](http://github.com/scalatra/scalatra/issues/29): FlashMap should be serializable +* [GH-34](http://github.com/scalatra/scalatra/issues/34): ScalatraTests.addFilter not compatible with Jetty 8 +* [GH-35](http://github.com/scalatra/scalatra/issues/35): Support for POST in ScalatraTests diff --git a/notes/about.markdown b/notes/about.markdown new file mode 100644 index 000000000..282cb0fa3 --- /dev/null +++ b/notes/about.markdown @@ -0,0 +1 @@ +[Scalatra](http://github.com/scalatra/scalatra) is a tiny, [Sinatra](http://sinatrarb.com/)-like web framework for Scala. diff --git a/project/build/ChecksumPlugin.scala b/project/build/ChecksumPlugin.scala deleted file mode 100644 index f5c2e9ab9..000000000 --- a/project/build/ChecksumPlugin.scala +++ /dev/null @@ -1,77 +0,0 @@ -import sbt._ -import Process._ -import scala.xml._ -import com.rossabaker.sbt.gpg._ -import java.io.File -import java.io.FileInputStream -import java.security.MessageDigest -import java.security.DigestInputStream - -// TODO make independent of GpgPlugin -trait ChecksumPlugin extends BasicManagedProject with GpgPlugin { - lazy val skipChecksum = systemOptional[Boolean]("checksum.skip", false).value - val checksumsConfig = config("checksums") - - override def artifacts = - if (skipChecksum) - super.artifacts - else - super.artifacts flatMap { artifact => - artifact.`type` match { - case "asc" => Seq(artifact) - case _ => artifact :: (List("md5", "sha1").map { ext => - Artifact(artifact.name, ext, artifact.extension+"."+ext, - artifact.classifier, Seq(checksumsConfig), None) - }) - } - } - - lazy val checksum = checksumAction - - def checksumAction = checksumTask(artifacts) - .dependsOn(makePom) - .describedAs("Calculates MD5 and SHA1 checksums") - - def checksumTask(artifacts: Iterable[Artifact]): Task = task { - if (skipChecksum) { - log.info("Skipping checksums") - None - } - else { - artifacts.toStream flatMap checksumArtifact firstOption - } - } - - def checksumArtifact(artifact: Artifact): Option[String] = { - val path = artifact2Path(artifact) - path.ext match { - case "asc" => None - case "md5" => None - case "sha1" => None - case _ => - Stream("md5", "sha1").flatMap { ext => - val md = MessageDigest.getInstance(ext); - val is = new FileInputStream(path asFile); - try { - // inefficient and ugly - val dis = new DigestInputStream(is, md); - while ( dis.read != -1) { - // this updates the associated digest - } - val data = dis.getMessageDigest.digest - val checksum = data map (x => "%02x".format(x)) mkString - - val outfile = path+"."+ext - log.info("Writing checksum to "+outfile) - FileUtilities.write(new File(outfile), checksum, log) - } - finally { - is close; - } - }.firstOption - } - } - - override def deliverLocalAction = super.deliverLocalAction dependsOn(checksum) - override def deliverAction = super.deliverAction dependsOn(checksum) -} diff --git a/project/build/GenerateChecksums.scala b/project/build/GenerateChecksums.scala new file mode 100644 index 000000000..42d401c9c --- /dev/null +++ b/project/build/GenerateChecksums.scala @@ -0,0 +1,32 @@ +import sbt._ + +import java.{util => ju} +import scala.collection.jcl.Conversions._ +import org.apache.ivy.plugins.resolver._ + +trait GenerateChecksums extends BasicManagedProject { + override def ivySbt = { + def setChecksums(resolver: DependencyResolver): Unit = resolver match { + case r: ChainResolver => + r.getResolvers foreach { + case child: DependencyResolver => setChecksums(child) + } + case r: RepositoryResolver => + r.setChecksums("sha1,md5") + } + + val i = super.ivySbt + i.withIvy { ivy => + ivy.getSettings.getResolvers.toList foreach { + case r: DependencyResolver => setChecksums(r) + } + } + i + } + + private implicit def juCollection2Iterable[A](c: ju.Collection[A]): Iterable[A] = { + val list = new ju.ArrayList[A](c.size) + list.addAll(c) + list + } +} diff --git a/project/build/ScalatraProject.scala b/project/build/ScalatraProject.scala index 85bcd18c4..a0168a46e 100644 --- a/project/build/ScalatraProject.scala +++ b/project/build/ScalatraProject.scala @@ -1,19 +1,21 @@ import sbt._ import scala.xml._ -import com.rossabaker.sbt.gpg._ +import java.io.File import org.fusesource.scalate.sbt._ class ScalatraProject(info: ProjectInfo) extends ParentProject(info) with MavenCentralTopLevelProject + with posterous.Publish { - override def shouldCheckOutputDirectories = false - val jettyGroupId = "org.eclipse.jetty" val jettyVersion = "7.3.0.v20110203" val slf4jVersion = "1.6.1" - val scalateVersion = "1.4.0" + val scalateVersion = buildScalaVersion match { + case "2.8.0" => "1.3.2" + case _ => "1.4.1" + } override def managedStyle = ManagedStyle.Maven val glassfishRepo = "Glassfish Repo" at "http://download.java.net/maven/glassfish" @@ -83,11 +85,21 @@ class ScalatraProject(info: ProjectInfo) with SiteGenWebProject with UnpublishedProject { + override val jettyPort = 8081 val scalatePage = "org.fusesource.scalate" % "scalate-page" % scalateVersion val jetty7 = jettyGroupId % "jetty-webapp" % jettyVersion % "test" val logback = "org.slf4j" % "slf4j-nop" % slf4jVersion % "runtime" val markdown = "org.fusesource.scalamd" % "scalamd" % "1.5" % "runtime" val description = "Runs www.scalatra.org" + + override lazy val generateSite = + if (scalateVersion.startsWith("1.3.")) + task { + log.info("sitegen only supported by Scalate 1.4.0 and above") + None + } + else + super.generateSiteAction } lazy val scalatraTest = project("test", "scalatra-test", new DefaultProject(_) with ScalatraSubproject { @@ -96,7 +108,7 @@ class ScalatraProject(info: ProjectInfo) }) lazy val scalatest = project("scalatest", "scalatra-scalatest", new DefaultProject(_) with ScalatraSubproject { - val scalatest = "org.scalatest" % "scalatest" % "1.2" % "compile" + val scalatest = "org.scalatest" % "scalatest" % "1.3" % "compile" val junit = "junit" % "junit" % "4.8.1" % "compile" val description = "ScalaTest support for the Scalatra test framework" }, scalatraTest) @@ -191,7 +203,12 @@ class ScalatraProject(info: ProjectInfo) } } + // Without this, scalatra-scalatest and scalatra-specs can't find scalatra-test val scalatraRepo = publishTo override def deliverProjectDependencies = Nil + + // Tweak posterous settings + override def postTitle(vers: String) = "%s %s".format("Scalatra", vers) + override def postTags = "Scalatra" :: crossScalaVersions.map { "Scala " + _ }.toList } diff --git a/project/build/SignWithOpenpgp.scala b/project/build/SignWithOpenpgp.scala new file mode 100644 index 000000000..ed89a971a --- /dev/null +++ b/project/build/SignWithOpenpgp.scala @@ -0,0 +1,85 @@ +package com.rossabaker.sbt.openpgp + +import _root_.sbt._ +import java.{util => ju} +import scala.collection.jcl.Conversions._ +import org.apache.ivy.plugins.resolver._ +import org.apache.ivy.plugins.signer._ +import org.apache.ivy.plugins.signer.bouncycastle._ + +/** + * Turns on Ivy's pgp mechanism. It assumes: + * + * - that the system property pgp.password is set + * + * - That bouncy castle is on the boot classpath. It must be available in + * the same class loader as Ivy. This can be achieved with a custom + * sbt.boot.properties file. See http://code.google.com/p/simple-build-tool/wiki/GeneralizedLauncher + * + * Failing either of these conditions, a warning is issued and publishing + * continues successfully. + */ +trait SignWithOpenpgp extends BasicManagedProject { + lazy val pgpName = "sbt-pgp" + lazy val pgpSecring: Option[String] = system[String]("pgpSecring").get + lazy val pgpKeyId: Option[String] = system[String]("pgp.keyId").get + lazy val pgpPassword: Option[String] = system[String]("pgp.password").get + + lazy val pgpSignatureGenerator: Either[String, SignatureGenerator] = + pgpPassword match { + case Some(password) => + try { + val gen = new OpenPGPSignatureGenerator + gen.setName(pgpName) + pgpSecring foreach gen.setSecring + pgpKeyId foreach gen.setKeyId + gen.setPassword(password) + Right(gen) + } + catch { + case e: NoClassDefFoundError => + Left("openpgp not on classpath: "+e.getMessage) + } + + case None => + Left("system property pgp.password not set") + } + + override def ivySbt = { + def setSigner(resolver: DependencyResolver): Unit = resolver match { + case r: ChainResolver => + r.getResolvers foreach { + case child: DependencyResolver => setSigner(child) + } + case r: RepositoryResolver => + r.setSigner(pgpName) + } + + val i = super.ivySbt + pgpSignatureGenerator.right map { gen => + i.withIvy { ivy => + val settings = ivy.getSettings + settings.addSignatureGenerator(gen) + settings.getResolvers.toList foreach { + case r: DependencyResolver => setSigner(r) + } + } + } + i + } + + override def publishAction = + task { + pgpSignatureGenerator.left map { reason => + log.warn("not signing artifacts: " + reason) + } + None + } && super.publishAction + + private implicit def juCollection2Iterable[A](c: ju.Collection[A]): Iterable[A] = { + val list = new ju.ArrayList[A](c.size) + list.addAll(c) + list + } +} + diff --git a/project/build/TestWith.scala b/project/build/TestWith.scala index 71ae0e368..74fda43bc 100644 --- a/project/build/TestWith.scala +++ b/project/build/TestWith.scala @@ -6,5 +6,13 @@ trait TestWith extends BasicScalaProject { def testWithTestClasspath: Seq[BasicScalaProject] = Nil override def testCompileAction = super.testCompileAction dependsOn((testWithTestClasspath.map(_.testCompile) ++ testWithCompileClasspath.map(_.compile)) : _*) override def testClasspath = (super.testClasspath /: (testWithTestClasspath.map(_.testClasspath) ++ testWithCompileClasspath.map(_.compileClasspath) ))(_ +++ _) - override def deliverProjectDependencies = super.deliverProjectDependencies ++ testWithTestClasspath.map(_.projectID % "test") + // Our test-with dependencies need to publish before we deliver ourselves... + override def dependencies = super.dependencies ++ testWithCompileClasspath ++ testWithTestClasspath + // ... but we still want them in test scope. + override def deliverProjectDependencies = { + val testDeps = testWithTestClasspath map { _.projectID } + super.deliverProjectDependencies map { dep => + if (testDeps.contains(dep)) { dep % "test" } else dep + } + } } diff --git a/project/build/centralSyncRequirements.scala b/project/build/centralSyncRequirements.scala index 0461cdc05..1c959e803 100644 --- a/project/build/centralSyncRequirements.scala +++ b/project/build/centralSyncRequirements.scala @@ -8,8 +8,13 @@ import sbt._ import scala.xml._ +import com.rossabaker.sbt.openpgp._ -trait MavenCentralProject extends BasicManagedProject { +trait MavenCentralProject + extends BasicManagedProject + with SignWithOpenpgp + with GenerateChecksums +{ def projectDescription: String = projectName.get.get override def pomExtra = super.pomExtra ++ ( diff --git a/project/plugins/Plugins.scala b/project/plugins/Plugins.scala index 8f27eaee2..120513a3a 100644 --- a/project/plugins/Plugins.scala +++ b/project/plugins/Plugins.scala @@ -2,6 +2,9 @@ import sbt._ class Plugins(info: ProjectInfo) extends PluginDefinition(info) { - val gpgPlugin = "com.rossabaker" % "sbt-gpg-plugin" % "0.1.1" - val scalatePlugin = "org.fusesource.scalate" % "sbt-scalate-plugin" % "1.4.0" + val scalatePlugin = "org.fusesource.scalate" % "sbt-scalate-plugin" % "1.4.1" + + val snuggletex_repo = "snuggletex_repo" at "http://www2.ph.ed.ac.uk/maven2" + val t_repo = "t_repo" at "http://tristanhunt.com:8081/content/groups/public/" + val posterous = "net.databinder" % "posterous-sbt" % "0.1.6" } diff --git a/socketio/src/main/scala/org/scalatra/socketio/SocketIOSupport.scala b/socketio/src/main/scala/org/scalatra/socketio/SocketIOSupport.scala index e3a42e10e..946e813c1 100644 --- a/socketio/src/main/scala/org/scalatra/socketio/SocketIOSupport.scala +++ b/socketio/src/main/scala/org/scalatra/socketio/SocketIOSupport.scala @@ -117,6 +117,10 @@ object SocketIOSupport { } +/** + * This interface is likely to change before 2.0.0. Please come to the + * mailing list or IRC before betting your project on this. + */ trait SocketIOSupport extends Handler with Initializable { self: ScalatraServlet => diff --git a/test/src/main/scala/org/scalatra/test/ScalatraTests.scala b/test/src/main/scala/org/scalatra/test/ScalatraTests.scala index 956ac0ffe..e54e9fbdb 100644 --- a/test/src/main/scala/org/scalatra/test/ScalatraTests.scala +++ b/test/src/main/scala/org/scalatra/test/ScalatraTests.scala @@ -4,10 +4,18 @@ import scala.util.DynamicVariable import java.net.URLEncoder.encode import org.eclipse.jetty.testing.HttpTester import org.eclipse.jetty.testing.ServletTester -import org.eclipse.jetty.servlet.{FilterHolder, FilterMapping, DefaultServlet, ServletHolder} +import org.eclipse.jetty.server.DispatcherType +import org.eclipse.jetty.servlet.{FilterHolder, DefaultServlet, ServletHolder} import java.nio.charset.Charset import javax.servlet.http.HttpServlet import javax.servlet.Filter +import java.util.EnumSet + +object ScalatraTests { + val DefaultDispatcherTypes: EnumSet[DispatcherType] = + EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC) +} +import ScalatraTests._ /** * Provides a framework-agnostic way to test your Scalatra app. You probably want to extend this with @@ -76,11 +84,17 @@ trait ScalatraTests { def addServlet(servlet: Class[_ <: HttpServlet], path: String) = tester.addServlet(servlet, path) - def addFilter(filter: Filter, path: String) = - tester.getContext().addFilter(new FilterHolder(filter), path, FilterMapping.DEFAULT) - - def addFilter(filter: Class[_ <: Filter], path: String) = - tester.addFilter(filter, path, FilterMapping.DEFAULT) + def addFilter(filter: Filter, path: String): FilterHolder = + addFilter(filter, path, DefaultDispatcherTypes) + def addFilter(filter: Filter, path: String, dispatches: EnumSet[DispatcherType]): FilterHolder = { + val holder = new FilterHolder(filter) + tester.getContext.addFilter(holder, path, dispatches) + holder + } + def addFilter(filter: Class[_ <: Filter], path: String): FilterHolder = + addFilter(filter, path, DefaultDispatcherTypes) + def addFilter(filter: Class[_ <: Filter], path: String, dispatches: EnumSet[DispatcherType]): FilterHolder = + tester.getContext.addFilter(filter, path, dispatches) @deprecated("renamed to addFilter") def routeFilter(filter: Class[_ <: Filter], path: String) = @@ -103,10 +117,16 @@ trait ScalatraTests { withResponse(httpRequest("POST", uri, Seq.empty, headers, body), f) // @todo support POST multipart/form-data for file uploads - def put(uri: String, params: Iterable[(String, String)] = Seq.empty, headers: Map[String, String] = Map.empty)(f: => Unit) = { - withResponse(httpRequest("PUT", uri, params, headers), f) - } - + def put(uri: String, params: Tuple2[String, String]*)(f: => Unit): Unit = + put(uri, params)(f) + def put(uri: String, params: Iterable[(String,String)])(f: => Unit): Unit = + put(uri, params, Map[String, String]())(f) + def put(uri: String, params: Iterable[(String,String)], headers: Map[String, String])(f: => Unit): Unit = + put(uri, toQueryString(params), Map("Content-Type" -> "application/x-www-form-urlencoded; charset=utf-8") ++ headers)(f) + def put(uri: String, body: String = "", headers: Map[String, String] = Map.empty)(f: => Unit) = + withResponse(httpRequest("PUT", uri, Seq.empty, headers, body), f) + // @todo support PUT multipart/form-data for file uploads + def delete(uri: String, params: Iterable[(String, String)] = Seq.empty, headers: Map[String, String] = Map.empty)(f: => Unit) = { withResponse(httpRequest("DELETE", uri, params, headers), f) } @@ -137,3 +157,4 @@ trait ScalatraTests { // So servletContext.getRealPath doesn't crash. tester.setResourceBase("./src/main/webapp") } + diff --git a/website/ext/scalate/Boot.scala b/website/ext/scalate/Boot.scala new file mode 100644 index 000000000..2cfea5dfe --- /dev/null +++ b/website/ext/scalate/Boot.scala @@ -0,0 +1,11 @@ +package scalate + +import org.fusesource.scalate._ + +class Boot(engine: TemplateEngine) { + def run: Unit = { + for (ssp <- engine.filter("ssp"); md <- engine.filter("markdown")) { + engine.pipelines += "ssp.md"-> List(ssp, md) + } + } +} \ No newline at end of file diff --git a/website/src/WEB-INF/scalate/layouts/default.scaml b/website/src/WEB-INF/scalate/layouts/default.scaml index 3dcf9fca9..586288eff 100644 --- a/website/src/WEB-INF/scalate/layouts/default.scaml +++ b/website/src/WEB-INF/scalate/layouts/default.scaml @@ -2,9 +2,12 @@ -@ val body: String !!! -%html +%html< %head %title= title - %body - #content + %link(rel="stylesheet" type="text/css" href={uri("/css/scalatra.css")}) + %body< + #navigation + = include("/_navigation.ssp.md") + #content< != body diff --git a/website/src/_navigation.ssp.md b/website/src/_navigation.ssp.md new file mode 100644 index 000000000..0ead5afaa --- /dev/null +++ b/website/src/_navigation.ssp.md @@ -0,0 +1,5 @@ +- [Home](${uri("/")}) +- [Getting Started](${uri("/getting-started.html")}) +- [Documentation](${uri("/documentation")}) +- [Community](${uri("/community.html")}) +- [Source](${uri("http://github.com/scalatra")}) diff --git a/website/src/css/scalatra.css b/website/src/css/scalatra.css new file mode 100644 index 000000000..0ab87a432 --- /dev/null +++ b/website/src/css/scalatra.css @@ -0,0 +1,15 @@ +#navigation { + font-size: 80%; +} + +#navigation ul { + margin: 0; + padding: 0; +} + +#navigation li { + display: inline; + list-style-type: none; + padding: 0 1.5em 0 0; +} + diff --git a/website/src/documentation/auth.page b/website/src/documentation/auth.page new file mode 100644 index 000000000..2362642cb --- /dev/null +++ b/website/src/documentation/auth.page @@ -0,0 +1,19 @@ +--- +title: Authentication + +--- name:content +# Authentication + +Scalatra comes with an authentication module. + +## Examples + +- [Example](http://gist.github.com/660701) +- [Basic authentication example](http://gist.github.com/732347) +found + +## Dependency + +To use it from an SBT project, add the following to your project: + + val auth = "org.scalatra" %% "scalatra-auth" % scalatraVersion diff --git a/website/src/documentation/core.page b/website/src/documentation/core.page new file mode 100644 index 000000000..f56b87756 --- /dev/null +++ b/website/src/documentation/core.page @@ -0,0 +1,282 @@ +--- +title: Core framework + +--- name:content +# Core framework + +## Routes + +In Scalatra, a route is an HTTP method paired with a URL matching pattern. + + get("/") { + // show something + } + + post("/") { + // submit/create something + } + + put("/") { + // update something + } + + delete("/") { + // delete something + } + +### Route order + +The first matching route is invoked. Routes are matched from the bottom up. +_This is the opposite of Sinatra._ Route definitions are executed as part +of a Scala constructor; by matching from the bottom up, routes can be +overridden in child classes. + +### Path patterns + +Path patterns add parameters to the `params` map. Repeated values are +accessible through the `multiParams` map. + +#### Named parameters + +Route patterns may include named parameters: + + get("/hello/:name") { + // Matches "GET /hello/foo" and "GET /hello/bar" + // params("name") is "foo" or "bar" +

Hello, {params("name")}

+ } + +#### Wildcards + +Route patterns may also include wildcard parameters, accessible through the +`splat` key. + + get("/say/*/to/*) { + // Matches "GET /say/hello/to/world" + multiParams("splat") # == Seq("hello", "world") + } + + get("/download/*.*) { + // Matches "GET /download/path/to/file.xml" + multiParams("splat") # == Seq("path/to/file", "xml") + } + +#### Regular expressions + +The route matcher may also be a regular expression. Capture groups are +accessible through the `captures` key. + + get("""^\/f(.*)/b(.*)""".r) { + // Matches "GET /foo/bar" + multiParams("captures") # == Seq("oo", "ar") + } + +#### Path patterns in the REPL + +If you want to experiment with path patterns, it's very easy in the REPL. + + scala> import org.scalatra.pattern._ + import org.scalatra.pattern._ + + scala> val pattern = PathPatternParser.parseFrom("/foo/:bar") + pattern: PathPattern = PathPattern(^/foo/([^/?]+)$,List(bar)) + + scala> pattern("/y/x") // doesn't match + res1: Option[MultiParams] = None + + scala> pattern("/foo/x") // matches + res2: Option[MultiParams] = Some(Map(bar -> ListBuffer(x))) + +Obligatory scolding: the REPL is not a substitute for proper unit tests! + +#### Rails-like pattern matching + +By default, route patterns parsing is based on Sinatra. Rails has a +similar, but not identical, syntax, based on Rack::Mount's Strexp. The path +pattern parser is resolved implicitly, and may be overridden if you prefer +an alternate syntax: + + class RailsLikeRouting extends ScalatraFilter { + implicit override val string2RouteMatcher(path: String) = + RailsPathPatternParser(path) + + get("/:file(.:ext)") { // matched Rails-style } + } + +### Conditions + +Routes may include conditions. A condition is any expression that returns +Boolean. Conditions are evaluated by-name each time the route matcher runs. + + get("/foo") { + // Matches "GET /foo" + } + + get("/foo", request.getRemoteHost == "127.0.0.1") { + // Overrides "GET /foo" for local users + } + +Multiple conditions can be chained together. A route must match all +conditions: + + get("/foo", request.getRemoteHost == "127.0.0.1", request.getRemoteUser == "admin") { + // Only matches if you're the admin, and you're localhost + } + +No path pattern is necessary. A route may consist of solely a condition: + + get(isMaintenanceMode) { +

Go away!

+ } + +### Actions + +Each route is followed by an action. An Action may return any value, which +is then rendered to the response according to the following rules: + +
+
`Array[Byte]`
+
If no content-type is set, it is set to `application/octet-stream`. The byte array is written to the response's output stream.
+ +
`NodeSeq`
+
If no content-type is set, it is set to`text/html`. The node sequence is converted to a string and written to the response's writer.
+ +
`Unit`
+
This signifies that the action has rendered the entire response, and no further action is taken.
+ +
Any
+
For any other value, if the content type is not set, it is set to `text/plain`. The value is converted to a string and written to the response's writer
. +
+ +This behavior may be customized for these or other return types by +overriding `renderResponse`. + +## Filters + +### Before filters + +Before filters are evaluated before each request within the same context as +the routes. + + before { + // Default all responses to text/html + contentType = "text/html" + } + +### After filters + +After filters are evaluated after each request, but before the action result +is rendered, within the same context as the routes. + + after { + if (response.status >= 500) + println("OMG! ONOZ!") + } + +## Halting + +To immediately stop a request within a filter or route: + + halt() + +You can also specify the status: + + halt(410) + +Or the body: + + halt("This will be the body") + +Or both: + + halt(401, "Go away!") + +## Passing + +A route can punt processing to the next matching route using pass. +Remember, unlike Sinatra, routes are matched from the bottom up. + + get("/guess/*") { + "You missed!" + } + + get("/guess/:who") { + params("who") match { + case "Frank" => pass() + case _ => "You got me!" + } + } + +The route block is immediately exited and control continues with the next +matching route. If no matching route is found, a 404 is returned. + +## Accessing the Servlet API + +### HttpServletRequest + +The request is available through the `request` variable. The request is +implicitly extended with the following methods: + +1. `body`: to get the request body as a string +2. `isAjax`: to detect AJAX requests +3. `cookies` and `multiCookies`: a Map view of the request's cookies +4. Implements `scala.collection.mutable.Map` backed by request attributes + +### HttpServletResponse + +The response is available through the `response` variable. + +### HttpSession + +The session is available through the `session` variable. The session +implicitly implements `scala.collection.mutable.Map` backed by session +attributes. To avoid creating a session, it may be accessed through +`sessionOption`. + +### ServletContext + +The servlet context is available through the `servletContext` variable. The +servlet context implicitly implements `scala.collection.mutable.Map` backed +by servlet context attributes. + +## Configuration + +The environment is defined by: +1. The `org.scalatra.environment` system property. +2. The `org.scalatra.environment` init property. +3. A default of `development`. + +If the environment starts with "dev", then `isDevelopmentMode` returns true. +This flag may be used by other modules, for example, to enable the Scalate +console. + +## Error handling + +Error handlers run within the same context as routes and before filters. + +### Not Found + +Whenever no route matches, the `notFound` handler is invoked: + + notFound { +

Not found. Bummer.

+ } + +### Error + +The `error` handler is invoked any time an exception is raised from a route +block or a filter. The throwable can be obtained from the `caughtThrowable` +instance variable. This variable is not defined outside the `error` block. + + error { + log.error(caughtThrowable) + redirect("http://www.sadtrombone.com/") + } + +## Flash scope + +Flash scope is available by mixing in `FlashMapSupport`, which provides a +mutable map named `flash`. Values put into flash scope during the current +request are stored in the session through the next request and then +discarded. This is particularly useful for messages when using the +[Post/Redirect/Get](http://en.wikipedia.org/wiki/Post/Redirect/Get) pattern. diff --git a/website/src/documentation/migration.page b/website/src/documentation/migration.page new file mode 100644 index 000000000..7e72f7c65 --- /dev/null +++ b/website/src/documentation/migration.page @@ -0,0 +1,18 @@ +--- +title: Migrating from previous versions + +--- name:content +# Migrating from previous versions + +## scalatra-2.0.0.M1 to scalatra-2.0.0.M2 + +1. Session has been retrofitted to a Map interface. `get` now returns an option instead of the value. +2. ScalaTest support has been split off into `scalatra-scalatest` module. ScalatraSuite moved to `org.scalatest.test.scalatest` package, and no longer extends FunSuite in order to permit mixing in a BDD trait. You may either use ScalatraFunSuite or explicitly extend FunSuite yourself. + +## Step to Scalatra + +Scalatra was renamed from Step to Scalatra to avoid a naming conflict with (an unrelated web framework)[http://sourceforge.net/stepframework]. scalatra-1.2.1 is identical to step-1.2.0 with the following exceptions: + +1. The package has changed from `com.thinkminimo.step` to `org.scalatra`. +1. The `Step` class has been renamed to `ScalatraServlet`. +1. All other `Step*` classes have been renamed to `Scalatra*`. diff --git a/website/src/documentation/scalate.page b/website/src/documentation/scalate.page new file mode 100644 index 000000000..dbcf5b358 --- /dev/null +++ b/website/src/documentation/scalate.page @@ -0,0 +1,38 @@ +--- +title: Templating with Scalate + +--- name:content +# Templating with Scalate + +Scalatra provides optional support for +[Scalate](http://scalate.fusesource.org/), a Scala template engine. + +1. Depend on scalatra-scalate.jar and a [slf4j +binding](http://www.slf4j.org/manual.html#binding). In your SBT build: + + val scalatraScalate = "org.scalatra" %% "scalatra-scalate" % scalatraVersion + val slf4jBinding = "ch.qos.logback" % "logback-classic" % "0.9.25" % runtime + +2. Extend your application with `ScalateSupport` + + import org.scalatra._ + import org.scalatra.scalate._ + + class MyApplication extends ScalatraServlet with ScalateSupport { + // .... + } + +3. A template engine is created as the `templateEngine` variable. This can +be used to render templates and call layouts. + + get("/") { + templateEngine.layout("index.scaml", "content" -> "yada yada yada") + } + +Additionally, `createRenderContext` may be used to create a render context +for the current request and response. + +Finally, the [Scalate +Console](http://scalate.fusesource.org/documentation/console.html) is +enabled in development mode to display any unhandled exceptions. + diff --git a/website/src/documentation/socketio.page b/website/src/documentation/socketio.page new file mode 100644 index 000000000..5fc0cf493 --- /dev/null +++ b/website/src/documentation/socketio.page @@ -0,0 +1,70 @@ +--- +title: Websocket and Comet support through Socket.IO + +--- name:content +# WebSocket and Comet support through Socket.IO + +Scalatra provides optional support for websockets and comet through +[socket.io](http://socket.io). We depend on [the socketio-java +project](http://code.google.com/p/socketio-java) to provide this support. + +1. Depend on the scalatra-socketio.jar. In your SBT build: + + val scalatraSocketIO = "org.scalatra" %% "scalatra-socketio" % scalatraVersion + +2. SocketIO mimics a socket connection so it's easiest if you just create a +socketio servlet at /socket.io/* + + import org.scalatra.ScalatraServlet + import org.scalatra.socketio.SocketIOSupport + + class MySocketIOServlet extends ScalatraServlet with SocketIOSupport { + // ... + } + +3. Setup the callbacks + + socketio { socket => + + socket.onConnect { connection => + // Do stuff on connection + } + + socket.onMessage { (connection, frameType, message) => + // Receive a message + // use `connection.send("string")` to send a message + // use `connection.broadcast("to send")` to send a message to all connected clients except the current one + // use `connection.disconnect` to disconnect the client. + } + + socket.onDisconnect { (connection, reason, message) => + // Do stuff on disconnection + } + } + +4. Add the necessary entries to web.xml + + + SocketIOServlet + com.example.SocketIOServlet + + flashPolicyServerHost + localhost + + + flashPolicyServerPort + 843 + + + flashPolicyDomain + localhost + + + flashPolicyPorts + 8080 + + + +When you want to use websockets with jetty the sbt build tool gets in the +way and that makes it look like the websocket stuff isn't working. If you +deploy the war to a jetty distribution everything should work as expected. diff --git a/website/src/documentation/test.page b/website/src/documentation/test.page new file mode 100644 index 000000000..f9eb0b5a5 --- /dev/null +++ b/website/src/documentation/test.page @@ -0,0 +1,58 @@ +--- +title: Testing your Scalatra application + +--- name:content +# Testing your Scalatra application + +Scalatra includes a test framework for writing the unit tests for your Scalatra application. The framework lets you send requests to your app and examine the response. It can be mixed into the test framework of your choosing; integration with [ScalaTest](http://www.scalatest.org/) and [Specs](http://code.google.com/p/specs/) is already provided. ScalatraTests supports HTTP GET/POST tests with or without request parameters and sessions. For more examples, please refer to core/src/test/scala. + +##ScalaTest + +### Dependencies + +- scalatra-scalatest + +### Code + +Mix in ShouldMatchers or MustMatchers to your taste... + + class MyScalatraServletTests extends ScalatraFunSuite with ShouldMatchers { + // `MyScalatraServlet` is your app which extends ScalatraServlet + addServlet(classOf[MyScalatraServlet], "/*") + + test("simple get") { + get("/path/to/something") { + status should equal (200) + body should include ("hi!") + } + } + } + +## Specs + +### Dependencies + +- scalatra-specs + +### Example + + object MyScalatraServletTests extends ScalatraSpecification { + addServlet(classOf[MyScalatraServlet], "/*") + + "MyScalatraServlet when using GET" should { + "/path/to/something should return 'hi!'" in { + get("/") { + status mustEqual(200) + body mustEqual("hi!") + } + } + } + } + +## Other test frameworks + +### Dependencies +- scalatra-test + +### Usage guide +Create an instance of org.scalatra.test.ScalatraTests. Be sure to call `start()` and `stop()` before and after your test suite. diff --git a/website/src/faq.page b/website/src/faq.page new file mode 100644 index 000000000..6a884cd11 --- /dev/null +++ b/website/src/faq.page @@ -0,0 +1,56 @@ +--- +title: FAQ + +--- name:content pipeline:jade,markdown +h1 FAQ + +h2 General questions + +dl + dt It looks neat, but is it production ready? + dd + :markdown + - It is use in the backend for [LinkedIn Signal](http://sna-projects.com/blog/2010/10/linkedin-signal-a-look-under-the-hood/). + + - [ChaCha](http://www.chacha.com/) is using it in multiple internal applications. + + - A project is in currently development to support a site with over one million unique users. + + dt Should I extend ScalatraServlet or ScalatraFilter? + + dd + :markdown + The main difference is the default behavior when a route is not found. + A filter will delegate to the next filter or servlet in the chain (as + configured by web.xml), whereas a ScalatraServlet will return a 404 + response. + + Another difference is that ScalatraFilter matches routes relative to + the WAR's context path. ScalatraServlet matches routes relative to + the servlet path. This allows you to mount multiple servlets under in + different namespaces in the same WAR. + + ### Use ScalatraFilter if: + - You are migrating a legacy application inside the same URL space + - You want to serve static content from the WAR rather than a dedicated web server + + ### Use ScalatraServlet if: + - You want to match routes with a prefix deeper than the context path. + +h2 sbt + +dl + dt + :markdown + Why am I getting errors like `foo is not a member of package org.blah`? + + dd + :markdown + sbt does not update your dependencies before compilation. Run `sbt update` and then retry your build. + + dt How can I prevent OutOfMemoryErrors in sbt? + dd + :markdown + Try changing your sbt shell script, as recommended by the [Lift Wiki](http://www.assembla.com/wiki/show/liftweb/Using_SBT): + + java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch.jar "$@" diff --git a/website/src/getting-started.page b/website/src/getting-started.page new file mode 100644 index 000000000..f84317d50 --- /dev/null +++ b/website/src/getting-started.page @@ -0,0 +1,30 @@ +--- +title: Getting started + +--- name:content +# Getting started + +1. Git-clone the prototype. Alternatively, download and extract a [tarball](https://github.com/scalatra/scalatra-sbt-prototype/tarball/master) or [zip](https://github.com/scalatra/scalatra-sbt-prototype/zipball/master). + + $ git clone git://github.com/scalatra/scalatra-sbt-prototype.git my-app + +2. Change directory into your clone. + + $ cd my-app + +3. Launch [sbt](http://code.google.com/p/simple-build-tool). + + $ sbt + +4. Fetch the dependencies. + + > update + +5. Start Jetty, enabling continuous compilation and reloading. + + > jetty-run + > ~prepare-webapp + +6. Browse to [http://localhost:8080/](http://localhost:8080/). + +7. Start hacking on `src/main/scala/MyScalatraFilter.scala`. diff --git a/website/src/index.page b/website/src/index.page index 640d7b7cc..d38e345af 100644 --- a/website/src/index.page +++ b/website/src/index.page @@ -18,3 +18,13 @@ framework for [Scala](http://www.scala-lang.org/). * *Java-interoperable*: Scalatra can call your existing Java libraries and deploys to your favorite servlet container. Migrate from Java at your own pace, if at all. * *Vibrant*: Scalatra has an active [community](community.html) of users and contributors. Come join us. + +## Example + + import org.scalatra._ + + class ScalatraExample extends ScalatraServlet { + get("/") { +

Hello, world!

+ } + }