7. 2.0 is
• full stack web framework for JVM
• high-productive
• asynch & reactive
• stateless
• HTTP-centric
• typesafe
• scalable
• open source
• part of Typesafe Stack 2.0
8. 2.0 is fun and high-productive
• fast turnaround: change you code and hit reload! :)
• browser error reporting
• db evolutions
• modular and extensible via plugins
• minimal bootstrap
• integrated test framework
• easy cloud deployment (e.g. Heroku)
9. 2.0 is asynch & reactive
• WebSockets
• Comet
• HTTP 1.1 chuncked responses
• composable streams handling
• based on event-driven, non-blocking Iteratee I/O
10. 2.0 is HTTP-centric
• based on HTTP and stateless
• doesn't fight HTTP or browser
• clean & easy URL design (via routes)
• designed to work with HTML5
“When a web framework starts an architecture fight
with the web, the framework loses.”
11. 2.0 is typesafe where it matters
}
• templates
• routes
• configs
• javascript (via Goggle Closure)
all are compiled
• LESS stylesheets
• CoffeeScript
+ browser error reporting
12. 2.0 getting started
1. download Play 2.0 binary package
2. add play to your PATH:
export PATH=$PATH:/path/to/play20
3. create new project:
$ play new myFirstApp
4. run the project:
$ cd myFirstApp
$ play run
13. 2.0 config
single conf/application.conf:
# This is the main configuration file for the application.
# The application languages
# ~~~~~
application.langs="en"
# Database configuration
# ~~~~~
# You can declare as many datasources as you want.
# By convention, the default datasource is named `default`
#
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
# db.default.user=sa
# db.default.password=
# Evolutions
# ~~~~~
# You can disable evolutions if needed
# evolutionplugin=disabled
...
14. 2.0 IDE support
Eclipse: $ eclipsify
IntelliJ IDEA: $ idea
Netbeans: add to plugins.sbt:
resolvers += {
"remeniuk repo" at "http://remeniuk.github.com/maven"
}
libraryDependencies += {
"org.netbeans" %% "sbt-netbeans-plugin" % "0.1.4"
}
$ play netbeans
15. 2.0 and SBT
Build.scala plugins.sbt
import sbt._ addSbtPlugin("play" % "sbt-plugin" % "2.0")
import Keys._
import PlayProject._
object ApplicationBuild extends Build {
val appName = "Your application"
val appVersion = "1.0"
val appDependencies = Seq(
// Add your project dependencies here,
)
val main = PlayProject(
appName, appVersion, appDependencies,
mainLang = SCALA
).settings(
// Add your own project settings here
)
}
16. 2.0 routes
# Home page
GET / controllers.Application.homePage()
GET /home controllers.Application.show(page = "home")
# Display a client.
GET /clients/all controllers.Clients.list()
GET /clients/:id controllers.Clients.show(id: Long)
# Pagination links, like /clients?page=3
GET /clients controllers.Clients.list(page: Int ?= 1)
# With regex
GET /orders/$id<[0-9]+> controllers.Orders.show(id: Long)
# 'name' is all the rest part of the url including '/' symbols
GET /files/*name controllers.Application.download(name)
17. 2.0 reversed routing
# Hello action
GET /helloBob controllers.Application.helloBob
GET /hello/:name controllers.Application.hello(name)
// Redirect to /hello/Bob
def helloBob = Action {
Redirect( routes.Application.hello("Bob") )
}
18. 2.0 actions
action: (play.api.mvc.Request => play.api.mvc.Result)
Action(parse.text) { request =>
Ok("<h1>Got: " + request.body + "</h1>").as(HTML).withSession(
session + ("saidHello" -> "yes")
).withHeaders(
CACHE_CONTROL -> "max-age=3600",
ETAG -> "xx"
).withCookies(
Cookie("theme", "blue")
)
}
val notFound = NotFound
val pageNotFound = NotFound(<h1>Page not found</h1>)
val badRequest = BadRequest(views.html.form(formWithErrors))
val oops = InternalServerError("Oops")
val anyStatus = Status(488)("Strange response type")
20. 2.0 templates
…are just functions ;)
views/main.scala.html: @(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
<section class="content">@content</section>
</body>
</html>
views/hello.scala.html: @(name: String = “Guest”)
@main(title = "Home") {
<h1>Welcome @name! </h1>
}
then from Scala class: val html = views.html.Application.hello(name)
21. 2.0 database evolutions
conf/evolutions/${x}.sql:
# Add Post
# --- !Ups
CREATE TABLE Post (
id bigint(20) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
content text NOT NULL,
postedAt date NOT NULL,
author_id bigint(20) NOT NULL,
FOREIGN KEY (author_id) REFERENCES User(id),
PRIMARY KEY (id)
);
# --- !Downs
DROP TABLE Post;
23. 2.0 access SQL data via Anorm
import anorm._
DB.withConnection { implicit c =>
val selectCountries = SQL("Select * from Country")
// Transform the resulting Stream[Row] to a List[(String,String)]
val countries = selectCountries().map(row =>
row[String]("code") -> row[String]("name")
).toList
}
// using Parser API
import anorm.SqlParser._
val count: Long = SQL("select count(*) from Country").as(scalar[Long].single)
val result:List[String~Int] = {
SQL("select * from Country")
.as(get[String]("name")~get[Int]("population") map { case n~p => (n,p) } *)
}
24. 2.0 and Akka
def index = Action { // using actors, coverting Akka Future to Play Promise
Async {
(myActor ? "hello").mapTo[String].asPromise.map { response =>
Ok(response)
}
}
}
def index = Action { // execute some task asynchronously
Async {
Akka.future { longComputation() }.map { result =>
Ok("Got " + result)
}
}
}
// schedule sending message 'tick' to testActor every 30 minutes
Akka.system.scheduler.schedule(0 seconds, 30 minutes, testActor, "tick")
// schedule a single task
Akka.system.scheduler.scheduleOnce(10 seconds) {
file.delete()
}
25. 2.0 streaming response
def index = Action {
val file = new java.io.File("/tmp/fileToServe.pdf")
val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file)
SimpleResult(
header = ResponseHeader(200, Map(CONTENT_LENGTH ->
file.length.toString)),
body = fileContent
)
}
// Play has a helper for the above:
def index = Action {
Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}
26. 2.0 chunked results
def index = Action {
val data = getDataStream
val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data)
ChunkedResult(
header = ResponseHeader(200),
chunks = dataContent
)
}
// Play has a helper for the ChunkedResult above:
Ok.stream(dataContent)
27. 2.0 Comet
lazy val clock: Enumerator[String] = {
val dateFormat = new SimpleDateFormat("HH mm ss")
Enumerator.fromCallback { () =>
Promise.timeout(Some(dateFormat.format(new Date)), 100 milliseconds)
}
}
def liveClock = Action {
Ok.stream(clock &> Comet(callback = "parent.clockChanged"))
}
and then in template:
<script type="text/javascript" charset="utf-8">
var clockChanged = function(time) { // do something }
</script>
<iframe id="comet" src="@routes.Application.liveClock.unique"></iframe>
28. 2.0 WebSockets
just another action in controller:
def index = WebSocket.using[String] { request =>
// Log events to the console
val in = Iteratee.foreach[String](println).mapDone { _ =>
println("Disconnected")
}
// Send a single 'Hello!' message
val out = Enumerator("Hello!")
(in, out)
}
29. 2.0 caching API
by default uses EHCache, configurable via plugins
Cache.set("item.key", connectedUser)
val user: User = Cache.getOrElse[User]("item.key") {
User.findById(connectedUser)
}
// cache HTTP response
def index = Cached("homePage",600) {
Action {
Ok("Hello world")
}
}
30. 2.0 i18n
conf/application.conf: application.langs="en,en-US,fr"
conf/messages.en:
home.title=File viewer
files.summary=The disk {1} contains {0} file(s).
from Scala class: val title = Messages("home.title")
val titleFR = Messages("home.title")(Lang(“fr"))
val summary = Messages("files.summary", d.files.length, d.name)
from template: <h1>@Messages("home.title")</h1>
31. 2.0 testing
…using specs2 by default
"Computer model" should {
"be retrieved by id" in {
running(FakeApplication(additionalConfiguration = inMemoryDatabase())) {
val Some(macintosh) = Computer.findById(21)
macintosh.name must equalTo("Macintosh")
macintosh.introduced must beSome.which(dateIs(_, "1984-01-24"))
}
}
}
32. 2.0 testing templates
"render index template" in {
val html = views.html.index("Coco")
contentType(html) must equalTo("text/html")
contentAsString(html) must contain("Hello Coco")
}
33. 2.0 testing controllers
"respond to the index Action" in {
val result = controllers.Application.index("Bob")(FakeRequest())
status(result) must equalTo(OK)
contentType(result) must beSome("text/html")
charset(result) must beSome("utf-8")
contentAsString(result) must contain("Hello Bob")
}
34. 2.0 testing routes
"respond to the index Action" in {
val Some(result) = routeAndCall(FakeRequest(GET, "/Bob"))
status(result) must equalTo(OK)
contentType(result) must beSome("text/html")
charset(result) must beSome("utf-8")
contentAsString(result) must contain("Hello Bob")
}
35. 2.0 testing server
"run in a server" in {
running(TestServer(3333)) {
await( WS.url("http://localhost:3333").get ).status must equalTo(OK)
}
}
36. 2.0 testing with browser
…using Selenium WebDriver with FluentLenium
"run in a browser" in {
running(TestServer(3333), HTMLUNIT) { browser =>
browser.goTo("http://localhost:3333")
browser.$("#title").getTexts().get(0) must equalTo("Hello Guest")
browser.$("a").click()
browser.url must equalTo("http://localhost:3333/Coco")
browser.$("#title").getTexts().get(0) must equalTo("Hello Coco")
}
}